Compare commits
4 Commits
aeebddd191
...
73b33f9fb5
Author | SHA1 | Date | |
---|---|---|---|
73b33f9fb5 | |||
85d9f1e280 | |||
3bb6f0374c | |||
15bff5af96 |
11
ui/package-lock.json
generated
11
ui/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "atsebayt",
|
"name": "atsebayt",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.5",
|
||||||
"svelte-frappe-charts": "^1.9.1",
|
"svelte-frappe-charts": "^1.9.1",
|
||||||
"vite": "^3.0.4"
|
"vite": "^3.0.4"
|
||||||
},
|
},
|
||||||
@ -645,6 +646,11 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
|
||||||
|
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@ -3091,6 +3097,11 @@
|
|||||||
"which": "^2.0.1"
|
"which": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dayjs": {
|
||||||
|
"version": "1.11.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz",
|
||||||
|
"integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA=="
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"dayjs": "^1.11.5",
|
||||||
"svelte-frappe-charts": "^1.9.1",
|
"svelte-frappe-charts": "^1.9.1",
|
||||||
"vite": "^3.0.4"
|
"vite": "^3.0.4"
|
||||||
}
|
}
|
||||||
|
21
ui/src/components/DateTimeInput.svelte
Normal file
21
ui/src/components/DateTimeInput.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export let format = 'YYYY-MM-DD HH:mm';
|
||||||
|
export let date = new Date();
|
||||||
|
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
export let id = null;
|
||||||
|
|
||||||
|
let internal;
|
||||||
|
|
||||||
|
const input = (x) => (internal = dayjs(x).format(format));
|
||||||
|
const output = (x) => (date = dayjs(x, format).toDate().toISOString());
|
||||||
|
|
||||||
|
$: input(date)
|
||||||
|
$: output(internal)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="datetime-local" class={className} id={id} bind:value={internal}>
|
@ -1,4 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import DateFormat from '../components/DateFormat.svelte';
|
import DateFormat from '../components/DateFormat.svelte';
|
||||||
import { getUserRendu } from '../lib/works';
|
import { getUserRendu } from '../lib/works';
|
||||||
|
|
||||||
@ -7,12 +9,32 @@
|
|||||||
|
|
||||||
export let work = null;
|
export let work = null;
|
||||||
export let user = null;
|
export let user = null;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
let renduP = null;
|
||||||
|
let submissionP = null;
|
||||||
|
|
||||||
|
if (work.submission_url != '-') {
|
||||||
|
if (work.submission_url) {
|
||||||
|
renduP = getUserRendu(work.submission_url, user);
|
||||||
|
renduP.then((rendu) => {
|
||||||
|
if (rendu !== null) {
|
||||||
|
dispatch('done');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
submissionP = work.getSubmission(user.id);
|
||||||
|
submissionP.then((submission) => {
|
||||||
|
dispatch('done');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if work.submission_url == '-'}
|
{#if work.submission_url == '-'}
|
||||||
<!-- Display nothing -->
|
<!-- Display nothing -->
|
||||||
{:else if work.submission_url}
|
{:else if work.submission_url}
|
||||||
{#await getUserRendu(work.submission_url, user)}
|
{#await renduP}
|
||||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
{:then rendu}
|
{:then rendu}
|
||||||
{#if rendu === null}
|
{#if rendu === null}
|
||||||
@ -24,7 +46,7 @@
|
|||||||
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
|
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
|
||||||
{/await}
|
{/await}
|
||||||
{:else}
|
{:else}
|
||||||
{#await work.getSubmission(user.id)}
|
{#await submissionP}
|
||||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
{:then submission}
|
{:then submission}
|
||||||
<i class="bi text-success bi-check-circle-fill" title={"Rendu effectué : " + JSON.stringify(submission)}></i>
|
<i class="bi text-success bi-check-circle-fill" title={"Rendu effectué : " + JSON.stringify(submission)}></i>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { getQuestions } from '../lib/questions';
|
import { getQuestions } from '../lib/questions';
|
||||||
|
import DateTimeInput from './DateTimeInput.svelte';
|
||||||
import { ToastsStore } from '../stores/toasts';
|
import { ToastsStore } from '../stores/toasts';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -13,17 +14,21 @@
|
|||||||
dispatch('saved', response);
|
dispatch('saved', response);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
ToastsStore.addErrorToast({
|
ToastsStore.addErrorToast({
|
||||||
msg: error.errmsg,
|
msg: error,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deleteInProgress = false;
|
||||||
function deleteSurvey() {
|
function deleteSurvey() {
|
||||||
|
deleteInProgress = true;
|
||||||
survey.delete().then((response) => {
|
survey.delete().then((response) => {
|
||||||
|
deleteInProgress = false;
|
||||||
goto(`surveys`);
|
goto(`surveys`);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
|
deleteInProgress = false;
|
||||||
ToastsStore.addErrorToast({
|
ToastsStore.addErrorToast({
|
||||||
msg: error.errmsg,
|
msg: error,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -33,7 +38,7 @@
|
|||||||
goto(`surveys/${response.id}`);
|
goto(`surveys/${response.id}`);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
ToastsStore.addErrorToast({
|
ToastsStore.addErrorToast({
|
||||||
msg: error.errmsg,
|
msg: error,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -104,7 +109,7 @@
|
|||||||
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
|
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<input type="text" class="form-control form-control-sm" id="start_availability" bind:value={survey.start_availability}>
|
<DateTimeInput class="form-control form-control-sm" id="start_availability" bind:date={survey.start_availability} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -113,7 +118,7 @@
|
|||||||
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
|
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<input type="text" class="form-control form-control-sm" id="end_availability" bind:value={survey.end_availability}>
|
<DateTimeInput class="form-control form-control-sm" id="end_availability" bind:date={survey.end_availability} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -137,7 +142,12 @@
|
|||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||||
{#if survey.id}
|
{#if survey.id}
|
||||||
<button type="button" class="btn btn-danger" on:click={deleteSurvey}>Supprimer</button>
|
<button type="button" class="btn btn-danger" on:click={deleteSurvey} disabled={deleteInProgress}>
|
||||||
|
{#if deleteInProgress}
|
||||||
|
<div class="spinner-border spinner-border-sm text-light me-1" role="status"></div>
|
||||||
|
{/if}
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-secondary" on:click={duplicateSurvey}>Dupliquer avec ces nouveaux paramètres</button>
|
<button type="button" class="btn btn-secondary" on:click={duplicateSurvey}>Dupliquer avec ces nouveaux paramètres</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import DateTimeInput from './DateTimeInput.svelte';
|
||||||
import { ToastsStore } from '../stores/toasts';
|
import { ToastsStore } from '../stores/toasts';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@ -12,7 +13,7 @@
|
|||||||
dispatch('saved', response);
|
dispatch('saved', response);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
ToastsStore.addErrorToast({
|
ToastsStore.addErrorToast({
|
||||||
msg: error.errmsg,
|
msg: error,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -22,7 +23,7 @@
|
|||||||
goto(`works`);
|
goto(`works`);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
ToastsStore.addErrorToast({
|
ToastsStore.addErrorToast({
|
||||||
msg: error.errmsg,
|
msg: error,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -32,7 +33,7 @@
|
|||||||
goto(`works/${response.id}`);
|
goto(`works/${response.id}`);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
ToastsStore.addErrorToast({
|
ToastsStore.addErrorToast({
|
||||||
msg: error.errmsg,
|
msg: error,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -102,7 +103,7 @@
|
|||||||
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
|
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
<input type="text" class="form-control form-control-sm" id="start_availability" bind:value={work.start_availability}>
|
<DateTimeInput class="form-control form-control-sm" id="start_availability" bind:date={work.start_availability} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -111,7 +112,7 @@
|
|||||||
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
|
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<input type="text" class="form-control form-control-sm" id="end_availability" bind:value={work.end_availability}>
|
<DateTimeInput class="form-control form-control-sm" id="end_availability" bind:date={work.end_availability} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -120,6 +120,12 @@ export class Question {
|
|||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
|
// Start by deleting proposals
|
||||||
|
const proposals = await this.getProposals();
|
||||||
|
for (const p of proposals) {
|
||||||
|
await p.delete();
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`api/questions/${this.id}`, {
|
const res = await fetch(`api/questions/${this.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { getCorrectionTemplates } from './correctionTemplates';
|
||||||
import { getQuestions } from './questions';
|
import { getQuestions } from './questions';
|
||||||
import { Response } from './response';
|
import { Response } from './response';
|
||||||
import { Work } from './works';
|
import { Work } from './works';
|
||||||
@ -101,9 +102,33 @@ export class Survey {
|
|||||||
// Now recopy questions
|
// Now recopy questions
|
||||||
const questions = await getQuestions(oldSurveyId);
|
const questions = await getQuestions(oldSurveyId);
|
||||||
for (const q of questions) {
|
for (const q of questions) {
|
||||||
|
const oldQuestionId = q.id;
|
||||||
|
|
||||||
delete q.id;
|
delete q.id;
|
||||||
q.id_survey = response.id;
|
q.id_survey = response.id;
|
||||||
q.save();
|
q.save().then((question) => {
|
||||||
|
q.id = oldQuestionId;
|
||||||
|
|
||||||
|
// Now recopy proposals
|
||||||
|
if (q.kind == "mcq" || q.kind == "ucq") {
|
||||||
|
q.getProposals().then((proposals) => {
|
||||||
|
for (const p of proposals) {
|
||||||
|
delete p.id;
|
||||||
|
p.id_question = question.id;
|
||||||
|
p.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now recopy correction templates
|
||||||
|
getCorrectionTemplates(oldQuestionId).then((cts) => {
|
||||||
|
for (const ct of cts) {
|
||||||
|
delete ct.id;
|
||||||
|
ct.id_question = question.id;
|
||||||
|
ct.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -115,6 +140,12 @@ export class Survey {
|
|||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
|
// Start by deleting questions
|
||||||
|
const questions = await getQuestions(this.id);
|
||||||
|
for (const q of questions) {
|
||||||
|
await q.delete();
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`api/surveys/${this.id}`, {
|
const res = await fetch(`api/surveys/${this.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
|
@ -13,20 +13,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { user } from '../../../../stores/user';
|
||||||
import StartStopLiveSurvey from '../../../../components/StartStopLiveSurvey.svelte';
|
import StartStopLiveSurvey from '../../../../components/StartStopLiveSurvey.svelte';
|
||||||
|
import SurveyAdmin from '../../../../components/SurveyAdmin.svelte';
|
||||||
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
|
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
|
||||||
import SurveyQuestions from '../../../../components/SurveyQuestions.svelte';
|
import SurveyQuestions from '../../../../components/SurveyQuestions.svelte';
|
||||||
import { getQuestions } from '../../../../lib/questions';
|
import { getQuestions } from '../../../../lib/questions';
|
||||||
|
|
||||||
export let surveyP;
|
export let surveyP;
|
||||||
|
let edit = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await surveyP then survey}
|
{#await surveyP then survey}
|
||||||
|
{#if $user && $user.is_admin}
|
||||||
|
<button class="btn btn-primary ms-1 float-end" on:click={() => { edit = !edit; } } title="Éditer"><i class="bi bi-pencil"></i></button>
|
||||||
<StartStopLiveSurvey
|
<StartStopLiveSurvey
|
||||||
{survey}
|
{survey}
|
||||||
class="ms-1 float-end"
|
class="ms-1 float-end"
|
||||||
on:update={() => goto(`surveys/${survey.id}/admin`)}
|
on:update={() => goto(`surveys/${survey.id}/admin`)}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<h2>
|
<h2>
|
||||||
<a href="surveys/{survey.id}" class="text-muted" style="text-decoration: none"><</a>
|
<a href="surveys/{survey.id}" class="text-muted" style="text-decoration: none"><</a>
|
||||||
@ -36,6 +42,10 @@
|
|||||||
<SurveyBadge class="ms-2" {survey} />
|
<SurveyBadge class="ms-2" {survey} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $user && $user.is_admin && edit}
|
||||||
|
<SurveyAdmin {survey} on:saved={() => edit = false} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#await getQuestions(survey.id)}
|
{#await getQuestions(survey.id)}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
@ -22,6 +22,14 @@
|
|||||||
import { getUsers } from '../../../lib/users';
|
import { getUsers } from '../../../lib/users';
|
||||||
|
|
||||||
export let work = null;
|
export let work = null;
|
||||||
|
let usersP = null;
|
||||||
|
work.then((w) => {
|
||||||
|
usersP = getUsers(w.promo, w.group);
|
||||||
|
usersP.then((users) => { nb_users = users.length; });
|
||||||
|
});
|
||||||
|
|
||||||
|
let nb_rendus = 0;
|
||||||
|
let nb_users = 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await work then w}
|
{#await work then w}
|
||||||
@ -29,12 +37,12 @@
|
|||||||
<h2>
|
<h2>
|
||||||
<a href="works/{w.id}" class="text-muted" style="text-decoration: none"><</a>
|
<a href="works/{w.id}" class="text-muted" style="text-decoration: none"><</a>
|
||||||
{w.title}
|
{w.title}
|
||||||
|
<small class="text-muted">Rendus {Math.trunc(nb_rendus/nb_users*100)} % ({nb_rendus}/{nb_users})</small>
|
||||||
</h2>
|
</h2>
|
||||||
<SurveyBadge class="ms-2" survey={w} />
|
<SurveyBadge class="ms-2" survey={w} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#await getUsers(w.promo, w.group)}
|
{#await usersP then users}
|
||||||
{:then users}
|
|
||||||
<table class="w-100 mb-5">
|
<table class="w-100 mb-5">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -48,7 +56,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><a href="users/{user.login}">{user.login}</a></td>
|
<td><a href="users/{user.login}">{user.login}</a></td>
|
||||||
<td>
|
<td>
|
||||||
<SubmissionStatus work={w} user={user} />
|
<SubmissionStatus work={w} user={user} on:done={() => { nb_rendus += 1; user.show_dl_btn = true; }} />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{#await getRepositories(w.id, user.id) then repos}
|
{#await getRepositories(w.id, user.id) then repos}
|
||||||
@ -82,6 +90,7 @@
|
|||||||
<a
|
<a
|
||||||
href="/api/users/{user.id}/works/{w.id}/download"
|
href="/api/users/{user.id}/works/{w.id}/download"
|
||||||
class="btn btn-sm btn-dark"
|
class="btn btn-sm btn-dark"
|
||||||
|
class:disabled={!user.show_dl_btn}
|
||||||
title="Télécharger la tarball du rendu"
|
title="Télécharger la tarball du rendu"
|
||||||
>
|
>
|
||||||
<i class="bi bi-download"></i>
|
<i class="bi bi-download"></i>
|
||||||
|
Reference in New Issue
Block a user