ui: Use $lib in imports

This commit is contained in:
nemunaire 2022-11-18 15:38:50 +01:00
commit d6f620bc0d
54 changed files with 146 additions and 146 deletions

View file

@ -0,0 +1,19 @@
<script>
import { page } from '$app/stores';
let className = '';
export { className as class };
let auth_route = 'auth/CRI'
$: {
if ($page.url.searchParams.get('next')) {
auth_route = 'auth/CRI?next=' + encodeURIComponent($page.url.searchParams.get('next'));
} else {
auth_route = 'auth/CRI?';
}
}
</script>
<a href={auth_route} target="_self" class="{className}">
<slot></slot>
</a>

View file

@ -0,0 +1,27 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let repo_pull_state = null
</script>
{#await repo_pull_state}
<div class="spinner-grow spinner-grow-sm mx-1" role="status"></div>
{:then state}
{#if state.status == "pending" || state.status == "running"}
<div
class="spinner-grow spinner-grow-sm mx-1"
class:text-primary={state.status == "pending"}
class:text-warning={state.status == "running"}
title="La récupération est en cours"
role="status"
></div>
{:else if state.status == "success"}
<i class="bi bi-check-circle-fill text-success mx-1" title="La récupération s'est bien passée"></i>
{:else if state.status == "failure" || state.status == "killed"}
<i class="bi bi-exclamation-circle-fill text-danger mx-1" title="La récupération ne s'est pas bien passée" style="cursor: pointer" on:click={() => dispatch('show_logs')}></i>
{:else}
{state.status}
{/if}
{/await}

View file

@ -0,0 +1,47 @@
<script>
import { createEventDispatcher } from 'svelte';
import CorrectionResponses from './CorrectionResponses.svelte';
export let question = null;
export let cts = null;
export let child = null;
export let filter = "";
export let notCorrected = false;
export let showStudent = false;
export let templates = [];
const dispatch = createEventDispatcher();
</script>
{#if question && (question.kind == 'mcq' || question.kind == 'ucq')}
{#await question.getProposals()}
<div class="text-center mt-4">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération des propositions&hellip;</span>
</div>
{:then proposals}
<CorrectionResponses
{cts}
bind:this={child}
{filter}
{question}
{notCorrected}
{proposals}
{showStudent}
{templates}
on:nb_responses={(v) => dispatch('nb_responses', v.detail)}
/>
{/await}
{:else}
<CorrectionResponses
{cts}
bind:this={child}
{filter}
{question}
{notCorrected}
{showStudent}
{templates}
on:nb_responses={(v) => dispatch('nb_responses', v.detail)}
/>
{/if}

View file

@ -0,0 +1,109 @@
<script lang="ts">
import Chart from 'svelte-frappe-charts';
let className = '';
export { className as class };
export let question = null;
function refreshProposals() {
let req = question.getProposals();
req.then((proposals) => {
const proposal_idx = { };
for (const proposal of proposals) {
data.labels.push(proposal.label);
data.datasets[0].values.push(0);
proposal_idx[proposal.id] = new String(data.labels.length - 1);
}
req_responses = question.getResponses();
req_responses.then((responses) => {
for (const res of responses) {
const rsplt = res.value.split(',');
for (const r of rsplt) {
data.datasets[0].values[proposal_idx[r]] += 1;
}
}
});
});
return req;
}
let req_proposals = null;
export let proposals = null;
let req_responses = null;
let mean = null;
export let data = {
labels: [],
datasets: [
{
values: []
}
]
};
if (!proposals) {
if (question.kind && (question.kind == "int" || question.kind.startsWith("list"))) {
req_responses = question.getResponses();
req_responses.then((responses) => {
const values = [];
const proposal_idx = { };
for (const response of responses) {
let ress = [];
if (question.kind.startsWith("list")) {
ress = response.value.split('\n');
} else {
ress.push(response.value);
}
for (const res of ress) {
if (res == "") continue;
if (proposal_idx[res]) {
data.datasets[0].values[proposal_idx[res]] += 1;
values.push(Number(res));
} else {
data.labels.push(res);
data.datasets[0].values.push(1);
proposal_idx[res] = new String(data.labels.length - 1);
}
}
}
if (question.kind == "int") {
mean = Math.trunc(values.reduce((p, e) => p + e) / values.length*10)/10;
}
});
} else {
req_proposals = refreshProposals();
}
}
</script>
<div class="{className}">
{#await req_proposals}
<div class="text-center mt-4">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération des propositions&hellip;</span>
</div>
{:then}
{#await req_responses}
<div class="text-center mt-4">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération des réponses&hellip;</span>
</div>
{:then}
{#if mean !== null}
<div class="text-center">
Moyenne représentative&nbsp;: <strong>{mean}</strong>
</div>
{/if}
{#if question.kind === "mcq"}
<Chart data={data} type="bar" />
{:else if question.kind === "list"}
<Chart data={data} type="pie" />
{:else}
<Chart data={data} type="pie" maxSlices="9" />
{/if}
{/await}
{/await}
</div>

View file

@ -0,0 +1,150 @@
<script>
import { CorrectionTemplate } from '$lib/correctionTemplates';
let className = '';
export { className as class };
export let cts = null;
export let nb_responses = 0;
export let question = null;
export let templates = [];
export let filter = "";
function addTemplate() {
const ct = new CorrectionTemplate()
if (question) {
ct.id_question = question.id;
}
templates.push(ct);
templates = templates;
}
function genTemplates() {
question.getProposals().then((proposals) => {
let i = 0;
for (const p of proposals) {
// Search proposal in templates
let found = false;
for (const tpl of templates) {
if (tpl.regexp.indexOf(p.id.toString()) !== -1) {
found = true;
break;
}
}
if (!found) {
const ct = new CorrectionTemplate()
ct.id_question = question.id;
ct.regexp = p.id.toString();
ct.label = String.fromCharCode(97 + i);
ct.save().then((ct) => {
templates.push(ct);
templates = templates;
});
}
i++;
}
})
}
function delTemplate(tpl) {
tpl.delete().then(() => {
const idx = templates.findIndex((e) => e.id === tpl.id);
if (idx >= 0) {
templates.splice(idx, 1);
}
templates = templates;
});
}
function submitTemplate(tpl) {
tpl.save().then(() => {
templates = templates;
});
}
</script>
<div class="{className}">
{#each templates as template (template.id)}
<form class="row mb-2" on:submit|preventDefault={() => submitTemplate(template)}>
<div class="col-2">
<div class="input-group">
<input
placeholder="RegExp"
class="form-control"
class:bg-warning={template.regexp && template.regexp === filter}
bind:value={template.regexp}
>
<button
type="button"
class="btn btn-sm"
class:btn-outline-secondary={!template.regexp || template.regexp !== filter}
class:btn-outline-warning={template.regexp && template.regexp === filter}
on:click={() => { if (filter == template.regexp) filter = ''; else filter = template.regexp; } }
>
<i class="bi bi-filter"></i>
</button>
</div>
</div>
<div class="col-2">
<input placeholder="Intitulé" class="form-control" bind:value={template.label}>
</div>
<div class="col-1">
<input
type="number"
placeholder="-12"
class="form-control"
bind:value={template.score}
>
</div>
<div class="col">
<textarea
placeholder="Explication pour l'étudiant"
class="form-control form-control-sm"
bind:value={template.score_explaination}
></textarea>
</div>
<div class="col-1 d-flex flex-column">
<div class="text-end">
{#if cts && template.id && cts[template.id.toString()]}
{Math.trunc(Object.keys(cts[template.id.toString()]).length/nb_responses*1000)/10}&nbsp;%
{:else}
N/A
{/if}
</div>
<div class="d-flex justify-content-between">
<button
type="button"
class="btn btn-sm btn-danger"
on:click={() => delTemplate(template)}
>
<i class="bi bi-trash"></i>
</button>
<button
class="btn btn-sm btn-success"
>
<i class="bi bi-check"></i>
</button>
</div>
</div>
</form>
{/each}
<button
type="button"
class="btn btn-info me-1"
on:click={addTemplate}
disabled={templates.length > 0 && !templates[templates.length-1].id}
>
<i class="bi bi-plus"></i> Ajouter un template
</button>
{#if question.kind == "mcq" || question.kind == "ucq"}
<button
type="button"
class="btn btn-outline-info me-1"
on:click={genTemplates}
>
<i class="bi bi-magic"></i> Générer les templates
</button>
{/if}
</div>

View file

@ -0,0 +1,124 @@
<script>
import { user } from '$lib/stores/user';
import { autoCorrection } from '$lib/correctionTemplates';
export let cts = null;
export let rid = 0;
export let response = null;
export let templates = [];
let my_tpls = { };
let my_correction = null;
function submitCorrection() {
if (response.score === undefined || response.score === null) {
if (my_correction && my_correction.score !== undefined) {
response.score = my_correction.score;
} else {
response.score = 100;
}
}
if (response.score_explaination === undefined || response.score_explaination === null) {
if (my_correction && my_correction.score_explaination !== undefined) {
response.score_explaination = my_correction.score_explaination;
}
}
response.id_corrector = $user.id
response.time_scored = (new Date()).toISOString()
response.save().then((res) => {
});
}
$: {
if (cts && templates && response && response.id_user) {
for (const t of templates) {
if (my_tpls[t.id] === undefined && cts[t.id.toString()]) {
my_tpls[t.id] = cts[t.id.toString()][response.id_user] !== undefined;
}
}
}
}
</script>
<form
class="row"
on:submit|preventDefault={submitCorrection}
>
<div class="col-auto">
<button
class="btn btn-success me-1"
class:mt-4={rid%2}
>
<i class="bi bi-check"></i>
</button>
</div>
<div class="col-7">
<div class="row row-cols-3">
{#each templates as template (template.id)}
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="r{response.id}t{template.id}"
on:change={() => {my_tpls[template.id] = !my_tpls[template.id]; autoCorrection(response.id_user, my_tpls).then((r) => my_correction = r); }}
checked={my_tpls[template.id]}
>
<label
class="form-check-label"
for="r{response.id}t{template.id}"
>
{template.label}
</label>
</div>
{/each}
</div>
</div>
<div class="col">
<div class="input-group mb-2">
<input
type="number"
class="form-control"
placeholder="Score"
bind:value={response.score}
>
{#if my_correction}
<button
type="button"
class="btn btn-light"
on:click={() => { response.score = my_correction.score; response.score_explaination = my_correction.score_explaination; }}
>
{my_correction.score}
</button>
{/if}
<span class="input-group-text">/100</span>
</div>
<textarea
class="form-control mb-2"
placeholder="Appréciation"
bind:value={response.score_explaination}
></textarea>
</div>
</form>
{#if my_correction}
<div
class="alert row mt-1 mb-0"
class:bg-success={my_correction.score > 100}
class:alert-success={my_correction.score >= 95 && my_correction.score <= 100}
class:alert-info={my_correction.score < 95 && my_correction.score >= 70}
class:alert-warning={my_correction.score < 70 && my_correction.score >= 45}
class:alert-danger={my_correction.score < 45}
>
<strong class="col-auto">
{my_correction.score}&nbsp;%
</strong>
<div class="col">
{my_correction.score_explaination}
</div>
</div>
{/if}

View file

@ -0,0 +1,137 @@
<script>
import { createEventDispatcher } from 'svelte';
import QuestionProposals from './QuestionProposals.svelte';
import ResponseCorrected from './ResponseCorrected.svelte';
import CorrectionResponseFooter from './CorrectionResponseFooter.svelte';
import { autoCorrection } from '$lib/correctionTemplates';
import { getUser } from '$lib/users';
export let cts = null;
export let filter = "";
export let question = null;
export let proposals = null;
export let notCorrected = false;
export let showStudent = false;
export let templates = false;
function refreshResponses() {
let req = question.getResponses();
req.then((res) => {
responses = res;
dispatch('nb_responses', res.length);
});
return req;
}
const dispatch = createEventDispatcher();
let req_responses = refreshResponses();
let responses = [];
let filteredResponses = [];
$:{
filteredResponses = responses.filter((r) => (notCorrected || r.time_scored <= r.time_reported || !r.time_scored) && (!filter || ((filter[0] == '!' && !r.value.match(filter.substring(1))) || r.value.match(filter))));
}
export async function applyCorrections() {
for (const r of filteredResponses) {
const my_correction = { };
let has_no_lost_answer = false;
let completed_correction = false;
for (const tpl of templates) {
if (tpl.score >= 0) has_no_lost_answer = true;
if (!tpl.regexp && tpl.label) continue;
if (tpl.regexp && (tpl.regexp[0] == '!' && !r.value.match(tpl.regexp.substring(1))) || r.value.match(tpl.regexp)) {
my_correction[tpl.id] = true;
completed_correction = true;
} else {
my_correction[tpl.id] = false;
}
}
// If no valid correction template AND valid answer is defined,
// don't consider the absence of match as valid answer.
if (!completed_correction && has_no_lost_answer) continue;
const auto = await autoCorrection(r.id_user, my_correction);
r.score = auto.score;
r.score_explaination = auto.score_explaination;
await r.save();
}
req_responses = refreshResponses();
}
</script>
{#await req_responses}
<div class="text-center mt-4">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération des réponses&hellip;</span>
</div>
{:then}
{#each filteredResponses as response, rid (response.id)}
<div class="row">
<div class="col">
<div class="card mt-3">
<div class="card-body">
{#if question.kind == 'mcq' || question.kind == 'ucq'}
{#if !proposals}
<div class="alert bg-danger">
Une erreur s'est produite, aucune proposition n'a été chargée
</div>
{:else}
<QuestionProposals
kind={question.kind}
prefixid={'r' + response.id}
{proposals}
readonly
value={response.value}
/>
{/if}
{:else}
<p
class="card-text"
style="white-space: pre-line"
>
{response.value}
</p>
{/if}
<ResponseCorrected
{response}
/>
</div>
<div class="card-footer">
<CorrectionResponseFooter
{cts}
{rid}
bind:response={response}
{templates}
/>
</div>
</div>
</div>
{#if showStudent}
<div class="col-auto">
<div class="text-center mt-2" style="max-width: 110px">
{#await getUser(response.id_user)}
<div class="spinner-border text-primary mx-3" role="status"></div>
{:then user}
<a href="/users/{user.login}">
<img class="img-thumbnail" src="https://photos.cri.epita.fr/thumb/{user.login}" alt="avatar {user.login}">
<div
class="text-truncate"
title={user.login}
>
{user.login}
</div>
</a>
{/await}
</div>
</div>
{/if}
</div>
{/each}
{/await}

View file

@ -0,0 +1,17 @@
<script>
export let date;
export let dateStyle = "long";
export let timeStyle = "long";
function formatDate(input, dateStyle, timeStyle) {
if (typeof input === 'string') {
input = new Date(input);
}
return new Intl.DateTimeFormat(undefined, {
dateStyle,
timeStyle,
}).format(input);
}
</script>
{formatDate(date, dateStyle, timeStyle)}

View file

@ -0,0 +1,26 @@
<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) => {
const d = dayjs(x, format).toDate();
if (d) {
date = d.toISOString();
}
};
$: input(date)
$: output(internal)
</script>
<input type="datetime-local" class={className} id={id} bind:value={internal}>

View file

@ -0,0 +1,43 @@
<script>
import { createEventDispatcher, tick } from 'svelte';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let qid = "";
export let kind = "list";
export let value = "";
export let placeholder = "";
let nb;
let mval = [];
$: mval = value?value.split('\n'):[];
$: {
nb = Number(kind.substring(4));
while (mval.length < nb) {
mval.push("");
}
}
</script>
{#each mval as v, i}
<input
id={qid + "l" + i}
class={className}
type="text"
bind:value={mval[i]}
{placeholder}
on:change={() => { value = mval.join('\n').trim(); dispatch("change"); }}
on:blur={() => { if (kind == "list" && i == mval.length - 1 && mval[i] == "") { mval.pop(); mval = mval; } }}
>
{/each}
{#if kind == "list" && (mval.length == 0 || mval[mval.length-1] != "")}
<input
class={className}
type="text"
{placeholder}
on:focus={() => { mval.push(""); mval = mval; tick().then(() => document.getElementById(qid + "l" + (mval.length-1)).focus()); }}
>
{/if}

View file

@ -0,0 +1,78 @@
<script>
import CorrectionPieChart from './CorrectionPieChart.svelte';
export let responses = {};
export let users = {};
let res = {};
let labels = [];
let graph_data = { labels, datasets:[{values: []}] };
$: {
res = { };
labels = [];
responses = responses;
for (const user in responses) {
for (const ures of responses[user].split('\n')) {
if (ures === "") continue;
if (res[ures] === undefined) {
res[ures] = [];
}
res[ures].push(user);
if (res[ures].length == 2) {
labels.push(ures);
}
}
}
labels.sort((a,b) => (res[a].length - res[b].length))
graph_data.labels = labels;
graph_data.datasets[0].values = labels.map((l) => res[l].length);
console.log(graph_data);
}
</script>
<CorrectionPieChart
question={{kind: 'list'}}
proposals={[]}
data={graph_data}
/>
<div class="card mb-4">
<ul class="list-group list-group-flush">
{#each labels as rep, rid (rid)}
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span>
{rep}
</span>
<div>
{#each res[rep] as user}
<a href="users/{user}" target="_blank" class="badge bg-dark rounded-pill">
{#if users && users[user]}
{users[user].login}
{:else}
{user}
{/if}
</a>
{/each}
</div>
</li>
{/each}
{#each Object.keys(res) as rep, rid (rid)}
{#if labels.indexOf(rep) == -1}
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span>
{rep}
</span>
<a href="users/{res[rep]}" target="_blank" class="badge bg-dark rounded-pill">
{#if users && users[res[rep]]}
{users[res[rep]].login}
{:else}
{res[rep]}
{/if}
</a>
</li>
{/if}
{/each}
</ul>
</div>

View file

@ -0,0 +1,158 @@
<script>
import { createEventDispatcher } from 'svelte';
import ListInput from './ListInput.svelte';
import QuestionHeader from './QuestionHeader.svelte';
import QuestionProposals from './QuestionProposals.svelte';
import ResponseCorrected from './ResponseCorrected.svelte';
import { user } from '$lib/stores/user';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let question;
export let qid;
export let response_history = null;
export let readonly = false;
export let corrections = null;
export let survey = null;
export let value = "";
export let edit = false;
function saveQuestion() {
question.save().then((response) => {
question.description = response.description;
question = question;
edit = false;
})
}
function editQuestion() {
edit = true;
}
</script>
<div class="card my-3 {className}">
<QuestionHeader
bind:question={question}
{qid}
{edit}
>
{#if $user && $user.is_admin}
<button type="button" class="btn btn-sm btn-danger ms-1 float-end" on:click={() => dispatch('delete')}>
<i class="bi bi-trash-fill"></i>
</button>
{#if edit}
<button type="button" class="btn btn-sm btn-success ms-1 float-end" on:click={saveQuestion}>
<i class="bi bi-check"></i>
</button>
{:else}
<button type="button" class="btn btn-sm btn-primary ms-1 float-end" on:click={editQuestion}>
<i class="bi bi-pencil"></i>
</button>
{/if}
{/if}
</QuestionHeader>
<slot></slot>
{#if question.kind != 'none'}
<div class="card-body">
{#if false && response_history}
<div class="d-flex justify-content-end mb-2">
<div class="col-auto">
Historique&nbsp;:
<select class="form-select">
<option value="new">Actuel</option>
{#each response_history as history (history.id)}
<option value={history.id}>{new Intl.DateTimeFormat('default', { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}).format(new Date(history.time_submit))}</option>
{/each}
</select>
</div>
</div>
{/if}
{#if edit}
{#if question.kind && (question.kind == 'text' || question.kind == 'int' || question.kind.startsWith('list'))}
<div class="form-group row">
<label class="col-2 col-form-label" for="q{qid}placeholder">Placeholder</label>
<div class="col">
<input class="form-control" id="q{qid}placeholder" bind:value={question.placeholder}>
</div>
</div>
{:else if question.kind}
{#if !question.id}
Veuillez enregistrer la question pour pouvoir ajouter des propositions.
{:else}
{#await question.getProposals()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des choix &hellip;</span>
</div>
{:then proposals}
<QuestionProposals
edit
id_question={question.id}
kind={question.kind}
{proposals}
readonly
live={survey && survey.direct !== null}
{corrections}
bind:value={value}
on:change={() => { dispatch("change"); }}
/>
{/await}
{/if}
{/if}
{:else if question.kind == 'mcq' || question.kind == 'ucq'}
{#await question.getProposals()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des choix &hellip;</span>
</div>
{:then proposals}
<QuestionProposals
kind={question.kind}
{proposals}
{readonly}
live={survey && survey.direct !== null}
{corrections}
bind:value={value}
on:change={() => { dispatch("change"); }}
/>
{/await}
{:else if readonly}
<p class="card-text alert alert-secondary" style="white-space: pre-line">{value}</p>
{:else if question.kind == 'int'}
<input
class="ml-5 col-sm-2 form-control"
type="number"
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
>
{:else if question.kind && question.kind.startsWith('list')}
<ListInput
class="ml-5 col-sm-2 form-control my-1"
kind={question.kind}
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
/>
{:else}
<textarea
class="form-control"
rows="6"
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
></textarea>
{/if}
{#if survey && survey.corrected && response_history}
<ResponseCorrected
response={response_history}
{survey}
/>
{/if}
</div>
{/if}
</div>

View file

@ -0,0 +1,50 @@
<script>
import { createEventDispatcher } from 'svelte';
import { user } from '$lib/stores/user';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let question = null;
export let qid = null;
export let edit = false;
export let nodescription = false;
</script>
<div class="card-header {className}">
<slot></slot>
{#if edit}
<div class="card-title row">
<label for="q{qid}title" class="col-auto col-form-label font-weight-bold">Titre&nbsp;:</label>
<div class="col"><input id="q{qid}title" class="form-control" bind:value={question.title}></div>
</div>
{:else}
<h4 class="card-title mb-0">{#if qid !== null}{qid + 1}. {/if}{question.title}</h4>
{/if}
{#if edit}
<div class="form-group row">
<label class="col-2 col-form-label" for="q{qid}kind">Type de réponse</label>
<div class="col">
<select class="form-select" id="q{qid}kind" bind:value={question.kind}>
<option value="text">Texte</option>
<option value="list">Liste de champs de texte</option>
<option value="list1">Liste de champs de texte (1 champ)</option>
<option value="list2">Liste de champs de texte (2 champs)</option>
<option value="list5">Liste de champs de texte (5 champs)</option>
<option value="int">Entier</option>
<option value="ucq">QCU</option>
<option value="mcq">QCM</option>
<option value="none">Rien</option>
</select>
</div>
</div>
<textarea class="form-control mb-2" bind:value={question.desc_raw} placeholder="Description de la question"></textarea>
{:else if question.description && !nodescription}
<p class="card-text mt-2">{@html question.description}</p>
{/if}
</div>

View file

@ -0,0 +1,134 @@
<script>
import { createEventDispatcher } from 'svelte';
import { QuestionProposal } from '$lib/questions';
export let edit = false;
export let proposals = [];
export let live = false;
export let kind = 'mcq';
export let prefixid = '';
export let readonly = false;
export let corrections = null;
export let id_question = 0;
export let value;
let valueCheck = [];
$: {
if (value) {
valueCheck = value.split(',');
}
}
const dispatch = createEventDispatcher();
function addProposal() {
const p = new QuestionProposal();
p.id_question = id_question;
proposals.push(p);
proposals = proposals;
}
</script>
<div class:d-flex={live} class:justify-content-around={live}>
{#each proposals as proposal, pid (proposal.id)}
<div class="form-check">
{#if kind == 'mcq'}
<input
type="checkbox"
class:btn-check={live}
class:form-check-input={!live}
disabled={readonly}
name={prefixid + 'proposal' + proposal.id_question}
id={prefixid + 'p' + proposal.id}
bind:group={valueCheck}
value={proposal.id?proposal.id.toString():''}
on:change={() => { value = valueCheck.join(','); dispatch("change"); }}
>
{:else}
<input
type="radio"
class:btn-check={live}
class:form-check-input={!live}
disabled={readonly}
name={prefixid + 'proposal' + proposal.id_question}
id={prefixid + 'p' + proposal.id}
bind:group={value}
value={proposal.id?proposal.id.toString():''}
on:change={() => { dispatch("change"); }}
>
{/if}
{#if edit}
<form on:submit|preventDefault={() => { proposal.save().then(() => proposal = proposal); } }>
<div class="input-group input-group-sm mb-2">
<input
type="text"
class="form-control"
bind:value={proposal.label}
on:input={() => proposal.changed = true}
>
<button
type="button"
class="btn btn-outline-danger"
tabindex="-1"
disabled={!proposal.id}
on:click={() => { proposal.delete().then(() => { proposals.splice(pid, 1); proposals = proposals; }); }}
>
<i class="bi bi-trash"></i>
</button>
<button
type="submit"
class="btn"
class:btn-success={proposal.changed}
class:btn-outline-success={!proposal.changed}
disabled={!proposal.changed}
>
<i class="bi bi-check"></i>
</button>
</div>
</form>
{:else}
<label
class:form-check-label={!live}
class:btn={live}
class:btn-lg={live}
class:btn-primary={live && !corrections && value && value.indexOf(proposal.id.toString()) != -1}
class:btn-outline-primary={live && !corrections && (!value || value.indexOf(proposal.id.toString()) == -1)}
class:btn-success={live && corrections && corrections[proposal.id] == 0}
class:btn-warning={live && corrections && corrections[proposal.id] != 0 && corrections[proposal.id] != -100 && value && value.indexOf(proposal.id.toString()) != -1}
class:btn-danger={live && corrections && corrections[proposal.id] == -100 && value && value.indexOf(proposal.id.toString()) != -1}
class:btn-outline-warning={live && corrections && corrections[proposal.id] != 0 && corrections[proposal.id] != -100 && (!value || value.indexOf(proposal.id.toString()) == -1)}
class:btn-outline-danger={live && corrections && corrections[proposal.id] == -100 && (!value || value.indexOf(proposal.id.toString()) == -1)}
for={prefixid + 'p' + proposal.id}
>
{proposal.label}
</label>
{/if}
</div>
{/each}
</div>
{#if edit}
{#if kind == 'mcq'}
<input
type="checkbox"
class="form-check-input"
disabled
checked
>
{:else}
<input
type="radio"
class="form-check-input"
disabled
checked
>
{/if}
<button
type="button"
class="btn btn-sm btn-link"
disabled={proposals.length > 0 && !proposals[proposals.length-1].id}
on:click={addProposal}
>
ajouter
</button>
{/if}

View file

@ -0,0 +1,125 @@
<script>
import { user } from '$lib/stores/user';
import { ToastsStore } from '$lib/stores/toasts';
export let response = null;
export let survey = null;
let reportInProgress = false;
function report() {
reportInProgress = true;
response.report(survey).then((res) => {
reportInProgress = false;
response.time_reported = res.time_reported;
if (res.time_reported >= res.time_scored) {
ToastsStore.addToast({
msg: "Ton signalement a bien été pris en compte.",
color: "success",
title: "Signaler une erreur de correction",
});
} else if (!res.time_reported) {
ToastsStore.addToast({
msg: "La correction de ta réponse n'est maintenant plus signalée, signalement annulé.",
color: "info",
title: "Signaler une erreur de correction",
});
} else {
ToastsStore.addErrorToast({
msg: "Quelque chose s'est mal passé lors du signalement du problème.\nSi le problème persiste, contacte directement ton professeur.",
});
}
}, (error) => {
reportInProgress = false;
ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant le signalement du problème : " + error + "\nSi le problème persiste, contacte directement ton professeur.",
});
})
}
</script>
{#if response.score !== undefined}
<div
class="alert row mb-0"
class:alert-success={response.score >= 95}
class:alert-info={response.score < 95 && response.score >= 70}
class:alert-warning={response.score < 70 && response.score >= 45}
class:alert-danger={response.score < 45}
>
<div class="col-auto">
<strong
title="Tu as obtenu un score de {response.score}&nbsp;%, ce qui correspond à {Math.trunc(response.score*10/5)/10}/20."
>
{response.score}&nbsp;%
</strong>
</div>
<div class="col">
{#if response.id_user == $user.id}
<button
type="button"
class="d-block btn btn-sm float-end"
class:btn-outline-success={!response.time_reported && response.score >= 95}
class:btn-outline-info={!response.time_reported && response.score < 95 && response.score >= 70}
class:btn-outline-warning={!response.time_reported && response.score < 70 && response.score >= 45}
class:btn-outline-danger={!response.time_reported && response.score < 45}
class:btn-success={response.time_reported && response.score >= 95}
class:btn-info={response.time_reported && response.score < 95 && response.score >= 70}
class:btn-warning={response.time_reported && response.score < 70 && response.score >= 45}
class:btn-danger={response.time_reported && response.score < 45}
title="Signaler un problème avec la correction"
disabled={reportInProgress}
on:click={report}
>
{#if reportInProgress}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:else if response.time_reported > response.time_scored}
<i class="bi bi-exclamation-octagon-fill"></i>
{:else}
<i class="bi bi-exclamation-octagon"></i>
{/if}
</button>
{:else if $user.is_admin && response.time_reported}
{#if response.time_reported > response.time_scored}
<i
class="float-end bi bi-exclamation-octagon-fill"
class:text-warning={response.score < 45}
class:text-danger={response.score >= 45}
></i>
{:else}
<i
class="float-end bi bi-exclamation-octagon"
class:text-warning={response.score < 45}
class:text-danger={response.score >= 45}
></i>
{/if}
{/if}
{#if response.score_explaination}
{response.score_explaination}
{:else if response.score === 100}
<i class="bi bi-check"></i>
{/if}
</div>
</div>
{:else if response && survey}
{#if response.value}
<div class="alert alert-dark text-danger row mb-0">
<div class="col-auto" style="margin: -0.4em; font-size: 2em;">
🤯
</div>
<div class="col">
<strong>Oups, tu sembles être passé entre les mailles du filet&nbsp;!</strong>
Cette question a bien été corrigée, mais une erreur s'est produite dans la correction de ta réponse.
<a href="mailto:nemunaire@nemunai.re?subject=Question non corrigée (questionnaire {survey.id})">Contacte ton enseignant</a> au plus vite.
</div>
</div>
{:else}
<div class="alert alert-danger row mb-0">
<div class="col-auto" style="margin: -0.4em; font-size: 2em;">
😟
</div>
<div class="col">
<strong>Tu n'as pas répondu à cette question.</strong>
Que s'est-il passé&nbsp;?
</div>
</div>
{/if}
{/if}

View file

@ -0,0 +1,31 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let survey = null;
</script>
{#if survey.direct !== null}
<a href="surveys/{survey.id}/live" class="btn btn-danger ms-1 float-end" title="Aller au direct"><i class="bi bi-film"></i></a>
<button
type="button"
class="btn btn-primary {className}"
title="Terminer le direct"
on:click={() => dispatch('end')}
>
<i class="bi bi-align-end"></i>
</button>
{:else}
<button
type="button"
class="btn btn-primary {className}"
title="Commencer le direct"
on:click={() => {survey.shown = true; survey.direct = 0; survey.start_availability = new Date(); survey.end_availability = new Date(Date.now() + 43200000); survey.save().then(() => dispatch('update'));}}
>
<i class="bi bi-align-start"></i>
</button>
{/if}

View file

@ -0,0 +1,77 @@
<script>
import { getSurveys } from '$lib/surveys';
import { getUsers, getGrades, getPromos } from '$lib/users';
export let promo = null;
</script>
{#await getPromos() then promos}
<div class="float-end me-2">
<select class="form-select" bind:value={promo}>
<option value={null}>tous</option>
{#each promos as promo, pid (pid)}
<option value={promo}>{promo}</option>
{/each}
</select>
</div>
{/await}
<h2>
Étudiants {#if promo !== null}{promo}{/if}
<small class="text-muted">Notes</small>
</h2>
{#await getSurveys()}
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des questionnaires corrigés&hellip;
</div>
{:then surveys}
{#await getGrades()}
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des notes&hellip;
</div>
{:then grades}
<div class="card mb-5">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Login</th>
{#each surveys as survey (survey.id)}
{#if survey.corrected && (promo === null || survey.promo == promo)}
<th><a href="surveys/{survey.id}" style="text-decoration: none">{survey.title}</a></th>
{/if}
{/each}
</tr>
</thead>
<tbody>
{#await getUsers()}
<tr>
<td colspan="20">
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des étudiants&hellip;
</div>
</td>
</tr>
{:then users}
{#each users as user (user.id)}
{#if promo === null || user.promo === promo}
<tr>
<td><a href="users/{user.id}" style="text-decoration: none">{user.id}</a></td>
<td><a href="users/{user.login}" style="text-decoration: none">{user.login}</a></td>
{#each surveys as survey (survey.id)}
{#if survey.corrected && (promo === null || survey.promo == promo)}
<td>{grades[user.id] && grades[user.id][survey.id]?grades[user.id][survey.id]:""}</td>
{/if}
{/each}
</tr>
{/if}
{/each}
{/await}
</tbody>
</table>
</div>
{/await}
{/await}

View file

@ -0,0 +1,56 @@
<script>
import { createEventDispatcher } from 'svelte';
import DateFormat from '$lib/components/DateFormat.svelte';
import { getUserRendu } from '$lib/works';
let className = '';
export { className as class };
export let work = 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>
{#if work.submission_url == '-'}
<!-- Display nothing -->
{:else if work.submission_url}
{#await renduP}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then rendu}
{#if rendu === null}
<i class="bi text-danger bi-exclamation-circle-fill" title="Rendu non réceptionné"></i>
{:else}
<i class="bi text-success bi-check-circle-fill" title={"Rendu effectué : " + JSON.stringify(rendu)}></i>
{/if}
{:catch error}
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
{/await}
{:else}
{#await submissionP}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then submission}
<i class="bi text-success bi-check-circle-fill" title={"Rendu effectué : " + JSON.stringify(submission)}></i>
{:catch error}
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
{/await}
{/if}

View file

@ -0,0 +1,157 @@
<script>
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import { getQuestions } from '$lib/questions';
import DateTimeInput from './DateTimeInput.svelte';
import { ToastsStore } from '$lib/stores/toasts';
const dispatch = createEventDispatcher();
export let survey = null;
function saveSurvey() {
survey.save().then((response) => {
dispatch('saved', response);
}, (error) => {
ToastsStore.addErrorToast({
msg: error,
});
})
}
let deleteInProgress = false;
function deleteSurvey() {
deleteInProgress = true;
survey.delete().then((response) => {
deleteInProgress = false;
goto(`surveys`);
}, (error) => {
deleteInProgress = false;
ToastsStore.addErrorToast({
msg: error,
});
})
}
function duplicateSurvey() {
survey.duplicate().then((response) => {
goto(`surveys/${response.id}`);
}).catch((error) => {
ToastsStore.addErrorToast({
msg: error,
});
})
}
</script>
<form on:submit|preventDefault={saveSurvey}>
{#if survey.id}
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="title" class="col-form-label col-form-label-sm">Identifiant du questionnaire</label>
</div>
<div class="col-sm-8">
<input type="text" class="form-control-plaintext form-control-sm" id="title" value={survey.id}>
</div>
</div>
{/if}
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="title" class="col-form-label col-form-label-sm">Titre du questionnaire</label>
</div>
<div class="col-sm-8">
<input type="text" class="form-control form-control-sm" id="title" bind:value={survey.title}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="promo" class="col-form-label col-form-label-sm">Promo</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input type="number" step="1" min="0" max="2068" class="form-control form-control-sm" id="promo" bind:value={survey.promo}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="group" class="col-form-label col-form-label-sm">Restreindre au groupe</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input class="form-control form-control-sm" id="group" bind:value={survey.group}>
</div>
</div>
{#if survey.id}
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="direct" class="col-form-label col-form-label-sm">Question en direct</label>
</div>
<div class="col-sm-8">
{#await getQuestions(survey.id) then questions}
<select id="direct" class="form-select form-select-sm" bind:value={survey.direct}>
<option value={null}>Pas de direct</option>
<option value={0}>Pause</option>
{#each questions as question (question.id)}
<option value={question.id}>{question.id} - {question.title}</option>
{/each}
</select>
{/await}
</div>
</div>
{/if}
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
</div>
<div class="col-sm-8">
<DateTimeInput class="form-control form-control-sm" id="start_availability" bind:date={survey.start_availability} />
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
</div>
<div class="col-8">
<DateTimeInput class="form-control form-control-sm" id="end_availability" bind:date={survey.end_availability} />
</div>
</div>
<div class="row row-cols-3 mx-1 my-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="shown" bind:checked={survey.shown}>
<label class="form-check-label" for="shown">
Afficher le questionnaire
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="corrected" bind:checked={survey.corrected}>
<label class="form-check-label" for="corrected">
Marqué comme corrigé
</label>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Enregistrer</button>
{#if survey.id}
<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>
{/if}
</div>
</div>
</form>
<hr>

View file

@ -0,0 +1,13 @@
<script>
export let survey;
let className = '';
export { className as class };
</script>
{#if survey.direct != null}<span class="badge bg-danger {className}">Direct</span>
{:else if survey.startAvailability() > Date.now()}<span class="badge bg-info {className}">Prévu</span>
{:else if survey.endAvailability() > Date.now()}<span class="badge bg-warning {className}">En cours</span>
{:else if !survey.__start_availability}<span class="badge bg-dark {className}">Nouveau</span>
{:else if !survey.corrected}<span class="badge bg-primary text-light {className}">Terminé</span>
{:else}<span class="badge bg-success {className}">Corrigé</span>
{/if}

View file

@ -0,0 +1,115 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '$lib/stores/user';
import DateFormat from '$lib/components/DateFormat.svelte';
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
import SubmissionStatus from '$lib/components/SubmissionStatus.svelte';
import { getSurveys } from '$lib/surveys';
import { getScore } from '$lib/users';
export let allworks = false;
let req_surveys = getSurveys(allworks);
export let direct = null;
req_surveys.then((surveys) => {
for (const survey of surveys) {
if (survey.direct != null) {
direct = survey;
}
}
});
function gotoSurvey(survey) {
if (survey.kind === "w") {
goto(`works/${survey.id}`);
} else if (survey.direct != null) {
goto(`surveys/${survey.id}/live`);
} else if ($user.is_admin) {
goto(`surveys/${survey.id}/responses`);
} else {
goto(`surveys/${survey.id}`);
}
}
</script>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>Intitulé</th>
<th>Date</th>
{#if $user}
<th>Score</th>
{/if}
</tr>
</thead>
{#await req_surveys}
<tr>
<td colspan="5" class="text-center py-3">
<div class="spinner-border mx-3" role="status"></div>
<span>Chargement des questionnaires &hellip;</span>
</td>
</tr>
{:then surveys}
<tbody style="cursor: pointer;">
{#each surveys as survey, sid (survey.kind + survey.id)}
{#if (survey.shown || survey.direct == null || ($user && $user.is_admin)) && (!$user || (!$user.was_admin || $user.promo == survey.promo) || $user.is_admin)}
{#if $user && $user.is_admin && (sid == 0 || surveys[sid-1].promo != survey.promo)}
<tr class="bg-info text-light">
<th colspan="5" class="fw-bold">
{survey.promo}
</th>
</tr>
{/if}
<tr on:click={e => gotoSurvey(survey)}>
<td>
{#if !survey.shown}<i class="bi bi-eye-slash-fill" title="Ce questionnaire n'est pas affiché aux étudiants"></i>{/if}
{survey.title}
{#if survey.group}<span class="badge bg-secondary">{survey.group}</span>{/if}
{#if $user && survey.kind === "w" && survey.startAvailability() < Date.now()}
<SubmissionStatus work={survey} user={$user} />
{/if}
<SurveyBadge {survey} class="float-end" />
</td>
{#if survey.startAvailability() > Date.now()}
<td title="Disponible à partir du {survey.start_availability}">
<DateFormat date={survey.start_availability} dateStyle="medium" timeStyle="medium" />
<i class="bi bi-arrow-bar-right"></i>
</td>
{:else}
<td title="Sera fermé le {survey.start_availability}">
<i class="bi bi-arrow-bar-left"></i>
<DateFormat date={survey.end_availability} dateStyle="medium" timeStyle="medium" />
</td>
{/if}
{#if $user}
{#if !survey.corrected}
<td>N/A</td>
{:else}
<td>
{#await getScore(survey)}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then score}
{score.score}
{:catch error}
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
{/await}
</td>
{/if}
{/if}
</tr>
{/if}
{/each}
</tbody>
{/await}
{#if $user && $user.is_admin}
<tfoot>
<tr>
<td colspan="4">
<a href="surveys/new" class="btn btn-sm btn-primary">Ajouter un questionnaire</a>
</td>
</tr>
</tfoot>
{/if}
</table>

View file

@ -0,0 +1,115 @@
<script>
import { user } from '$lib/stores/user';
import { ToastsStore } from '$lib/stores/toasts';
import QuestionForm from '$lib/components/QuestionForm.svelte';
import { Question } from '$lib/questions';
export let survey = null;
export let id_user = null;
export let questions = [];
let newquestions = [];
let submitInProgress = false;
function submitAnswers() {
submitInProgress = true;
const res = [];
for (const r in responses) {
res.push({
"id": responses[r].id,
"id_question": responses[r].id_question,
"value": String(responses[r].value)
})
}
survey.submitAnswers(res, id_user).then((response) => {
submitInProgress = false;
ToastsStore.addToast({
msg: "Vos réponses ont bien étés sauvegardées.",
color: "success",
title: "Questionnaire",
});
}, (error) => {
submitInProgress = false;
ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez réessayer dans quelques instants.",
});
});
}
function addQuestion() {
const q = new Question();
q.id_survey = survey.id;
newquestions.push(q);
newquestions = newquestions;
}
function deleteQuestion(question, qid) {
question.delete().then(() => {
questions.splice(qid, 1);
questions = questions;
})
}
function deleteNewQuestion(question, qid) {
if (question.id) {
question.delete().then(() => {
newquestions.splice(qid, 1);
newquestions = newquestions;
})
} else {
newquestions.splice(qid, 1);
newquestions = newquestions;
}
}
let responses = {};
for (const q of questions) {
responses[q.id] = {id_question: q.id, value: ""};
}
survey.retrieveAnswers(id_user).then((response) => {
if (response) {
for (const res of response.reverse()) {
responses[res.id_question] = res;
}
}
})
</script>
<form class="mb-5" on:submit|preventDefault={submitAnswers}>
{#each questions as question, qid (question.id)}
<QuestionForm
{survey}
qid={qid}
question={question}
response_history={responses[question.id]}
readonly={survey.isFinished() && !$user.is_admin}
on:delete={() => deleteQuestion(question, qid)}
bind:value={responses[question.id].value}
/>
{/each}
{#each newquestions as question, qid (qid)}
<QuestionForm
qid={questions.length + qid}
question={question}
edit
on:delete={() => deleteNewQuestion(question, qid)}
/>
{/each}
<div class="d-flex justify-content-between">
{#if !survey.corrected || $user.is_admin}
<button type="submit" class="btn btn-primary" disabled={submitInProgress || (survey.isFinished() && !$user.is_admin)}>
{#if submitInProgress}
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
{/if}
Soumettre les réponses
</button>
{/if}
{#if $user && $user.is_admin}
<button type="button" class="btn btn-info" on:click={addQuestion}>
Ajouter une question
</button>
{/if}
</div>
</form>

View file

@ -0,0 +1,18 @@
<script>
import { ToastsStore } from '$lib/stores/toasts';
</script>
<div class="toast-container position-fixed top-0 end-0 p-3">
{#each $ToastsStore.toasts as toast}
<div class="toast show" role="alert">
<div class="toast-header">
<div class="bg-{toast.color} rounded me-2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
<strong>{#if toast.title}{toast.title}{:else}Questionnaire{/if}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{toast.msg}
</div>
</div>
{/each}
</div>

View file

@ -0,0 +1,52 @@
<script lang="ts">
import { getKeys, getKey, Key } from '$lib/key';
export let student = null;
</script>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Informations</th>
</tr>
</thead>
<tbody>
{#await getKeys(student.id)}
<tr>
<td colspan="4">
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des clefs&hellip;
</div>
</td>
</tr>
{:then keys}
{#if keys && keys.length > 0}
{#each keys as keyid}
{#await getKey(keyid, student.id)}
Veuillez patienter
{:then key}
<tr>
<td>{key.id}</td>
<td>{key.type.toUpperCase()}</td>
<td>
<dl>
{#each Object.keys(key.infos) as k}
<dt class="float-start me-3 my-0 py-0">{k}</dt>
<dd>{#if key.infos[k]}{key.infos[k]}{:else}<span class="fst-italic">-</span>{/if}</dd>
{/each}
</dl>
</td>
</tr>
{/await}
{/each}
{:else}
<tr>
<td colspan="4" class="text-center fst-italic">Cet utilisateur n'a pas défini de clef</td>
</tr>
{/if}
{/await}
</tbody>
</table>

View file

@ -0,0 +1,61 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getSurveys } from '$lib/surveys';
import { getUser, getUserGrade, getUserScore } from '$lib/users';
export let student = null;
export let allPromos = false;
</script>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Titre</th>
<th>Promo</th>
<th>Avancement</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{#await getSurveys()}
<tr>
<td colspan="4">
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des questionnaires&hellip;
</div>
</td>
</tr>
{:then surveys}
{#each surveys as survey, sid (survey.id)}
{#if allPromos || survey.promo === student.promo}
<tr on:click={e => goto(`users/${student.id}/surveys/${survey.id}`)}>
<td>{survey.id}</td>
<td>{survey.title}</td>
<td>{survey.promo}</td>
{#await getUserGrade(student.id, survey)}
<td>
<div class="spinner-border spinner-border-sm" role="status"></div>
</td>
{:then gr}
<td title="{gr.grades}">
{gr.avancement * 100}&nbsp;%
</td>
{/await}
{#await getUserScore(student.id, survey)}
<td>
<div class="spinner-border spinner-border-sm" role="status"></div>
</td>
{:then score}
<td>
{score.score}{#if score.score >= 0}/20{/if}
</td>
{/await}
</tr>
{/if}
{/each}
{/await}
</tbody>
</table>

View file

@ -0,0 +1,60 @@
<script>
import { user } from '$lib/stores/user';
import DateFormat from '$lib/components/DateFormat.svelte';
let className = '';
export { className as class };
const rendus_baseurl = "https://adlin.nemunai.re/rendus/";
async function getUserRendus() {
const res = await fetch(`${rendus_baseurl}${$user.login}.json`)
if (res.status == 200) {
return await res.json();
} else {
return null;
}
}
</script>
<table class="table {className}">
{#await getUserRendus()}
Please wait...
{:then rendus}
<thead>
<tr>
{#each Object.keys(rendus) as renduname, rid (rid)}
<th>{renduname}</th>
{/each}
</tr>
</thead>
<tbody>
<tr>
{#each Object.keys(rendus) as renduname, rid (rid)}
<th
class:bg-danger={!rendus[renduname]}
class:text-center={!rendus[renduname]}
class:bg-success={rendus[renduname]}
>
{#if rendus[renduname]}
<DateFormat date={rendus[renduname].date} dateStyle="medium" timeStyle="medium" /><br>
<span class="hash" title={rendus[renduname].hash}>{rendus[renduname].hash}</span>
{:else}
&ndash;
{/if}
</th>
{/each}
</tr>
</tbody>
{/await}
</table>
<style>
.hash {
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
</style>

View file

@ -0,0 +1,154 @@
<script>
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import DateTimeInput from './DateTimeInput.svelte';
import { ToastsStore } from '$lib/stores/toasts';
const dispatch = createEventDispatcher();
export let work = null;
function saveWork() {
work.save().then((response) => {
dispatch('saved', response);
}, (error) => {
ToastsStore.addErrorToast({
msg: error,
});
})
}
function deleteWork() {
work.delete().then((response) => {
goto(`works`);
}, (error) => {
ToastsStore.addErrorToast({
msg: error,
});
})
}
function duplicateWork() {
work.duplicate().then((response) => {
goto(`works/${response.id}`);
}).catch((error) => {
ToastsStore.addErrorToast({
msg: error,
});
})
}
</script>
<form on:submit|preventDefault={saveWork}>
{#if work.id}
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="title" class="col-form-label col-form-label-sm">Identifiant du travail</label>
</div>
<div class="col-sm-8">
<input type="text" class="form-control-plaintext form-control-sm" id="title" value={work.id}>
</div>
</div>
{/if}
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="title" class="col-form-label col-form-label-sm">Titre du travail</label>
</div>
<div class="col-sm-8">
<input type="text" class="form-control form-control-sm" id="title" bind:value={work.title}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="promo" class="col-form-label col-form-label-sm">Promo</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input type="number" step="1" min="0" max="2068" class="form-control form-control-sm" id="promo" bind:value={work.promo}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="group" class="col-form-label col-form-label-sm">Restreindre au groupe</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input class="form-control form-control-sm" id="group" bind:value={work.group}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="tagprefix" class="col-form-label col-form-label-sm">Préfixe des tag à regarder</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input class="form-control form-control-sm" id="tagprefix" bind:value={work.tag}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="submissionurl" class="col-form-label col-form-label-sm">URL validation la soumission</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input class="form-control form-control-sm" id="submissionurl" bind:value={work.submission_url}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
</div>
<div class="col-sm-8">
<DateTimeInput class="form-control form-control-sm" id="start_availability" bind:date={work.start_availability} />
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
</div>
<div class="col-8">
<DateTimeInput class="form-control form-control-sm" id="end_availability" bind:date={work.end_availability} />
</div>
</div>
<div class="row mt-2">
<div class="col-sm-3 text-sm-end">
<label for="work_description" class="col-form-label col-form-label-sm">Description</label>
</div>
<div class="col-8">
<textarea class="form-control" id="work_description" bind:value={work.descr_raw}></textarea>
</div>
</div>
<div class="row row-cols-3 mx-1 my-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="shown" bind:checked={work.shown}>
<label class="form-check-label" for="shown">
Afficher le travail
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="corrected" bind:checked={work.corrected}>
<label class="form-check-label" for="corrected">
Marqué comme corrigé
</label>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Enregistrer</button>
{#if work.id}
<button type="button" class="btn btn-danger" on:click={deleteWork}>Supprimer</button>
<button type="button" class="btn btn-secondary" on:click={duplicateWork}>Dupliquer avec ces nouveaux paramètres</button>
{/if}
</div>
</div>
</form>

View file

@ -0,0 +1,190 @@
<script>
import { createEventDispatcher } from 'svelte';
import BuildState from '$lib/components/BuildState.svelte';
import DateFormat from '$lib/components/DateFormat.svelte';
import { WorkRepository, getRemoteRepositories, getRepositories } from '$lib/repositories';
import { ToastsStore } from '$lib/stores/toasts';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let work = {};
export let user = null;
export let readonly = false;
let repo_work = null;
let repo_used = null;
let remote_repos = [];
let repo_pull_state = null;
let show_logs = null;
export let already_used = false;
function updatePullState(repo) {
repo_pull_state = repo.getBuildState();
show_logs = null;
repo_pull_state.then((state) => {
if (state.status == "pending" || state.status == "running") {
setTimeout(() => updatePullState(repo), state.status == "pending" ? 1000 : 3000);
} else if (state.status == "success") {
dispatch('update_submission');
}
})
}
function showLogs(repo) {
show_logs = repo.getBuildLogs()
}
function refresh_repo_work(user) {
if (user != null) {
repo_work = getRepositories(work.id, user.id);
} else {
repo_work = getRepositories(work.id);
}
repo_work.then((repos) => {
repo_used = repos[0];
already_used = repos[0].already_used == true;
updatePullState(repos[0])
}, () => {
repo_used = new WorkRepository({id_work: work.id});
already_used = false;
remote_repos = getRemoteRepositories(user?user.id:null);
});
}
$: refresh_repo_work(user);
let submitInProgress = false;
function submitWorkRepository() {
submitInProgress = true;
repo_used.save(user).then(() => {
submitInProgress = false;
refresh_repo_work(user);
}, (error) => {
submitInProgress = false;
ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant la création de la liaison au dépôt : " + error,
});
});
}
function goRetrieveWork(repo) {
submitInProgress = true;
repo.retrieveWork().then(() => {
submitInProgress = false;
refresh_repo_work(user);
}, (error) => {
submitInProgress = false;
ToastsStore.addErrorToast({msg: "Une erreur s'est produite : " + error});
})
}
</script>
{#await repo_work}
<div class="{className} text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement de vos préférences de rendu &hellip;</span>
</div>
{:then repos}
{#each repos as repo (repo.id)}
<div class="{className} card">
<div class="card-body d-flex justify-content-between">
<div class="d-flex flex-column justify-content-center pe-3">
<div>
<div class="row">
<label for={repo.id + "url"} class="col-sm-4 col-form-label">Dépôt lié&nbsp;:</label>
<div class="col-sm-8">
<input type="text" class="form-control form-control-sm" style="font-family: monospace" disabled id={repo.id + "url"} value={repo.uri}>
</div>
</div>
<p class="mt-2 mb-0 pe-1">
Vous pouvez ajouter un <span class="fst-italic">webhook</span> sur les <span class="fst-italic"><strong>Tag push events</strong></span> afin d'automatiser la récupération de votre travail. Dans les paramètres de votre dépôt sur GitLab, faite pointer un webhook sur <code>https://lessons.nemunai.re/api/callbacks/trigger.json</code> avec le secret ci-dessous.
</p>
<div class="row">
<label for={repo.id + "secret"} class="col-sm-4 col-form-label">Webhook Secret token&nbsp;:</label>
<div class="col-sm-8">
<div class="input-group">
<input type={repo.show_secret?"text":"password"} class="form-control form-control-sm" disabled id={repo.id + "secret"} value={repo.secret}>
<button class="btn btn-sm btn-outline-info" on:click={() => { repo.show_secret = !repo.show_secret}}>
<i class="bi" class:bi-eye={!repo.show_secret} class:bi-eye-slash={repo.show_secret}></i>
</button>
</div>
</div>
</div>
Dernière récupération&nbsp;: <strong>{#if repo.last_check}<DateFormat date={new Date(repo.last_check)} dateStyle="medium" timeStyle="medium" />{:else}-{/if}</strong>
{#if repo_pull_state}
<BuildState {repo_pull_state} on:show_logs={() => showLogs(repo)} />
{/if}
</div>
</div>
<div class="d-flex flex-column justify-content-center">
<button
type="button"
class="btn btn-outline-info float-end mb-1"
disable={submitInProgress || readonly}
on:click={() => goRetrieveWork(repo)}
>
Récupérer mon travail
</button>
<button
type="button"
class="btn btn-outline-danger float-end mt-1"
disable={submitInProgress || readonly}
on:click={() => repo.delete().then(() => { refresh_repo_work(user) }, (error) => ToastsStore.addErrorToast({msg: "Une erreur s'est produite durant la suppression du lien : " + error}))}
>
Supprimer cette liaison
</button>
</div>
</div>
{#if show_logs}
{#await show_logs then logs}
<div class="card-body d-flex justify-content-between bg-dark text-light">
<pre class="pb-2">{#each logs as l (l.pos)}{l.out}{/each}</pre>
</div>
{/await}
{/if}
</div>
{/each}
{:catch}
{#if !readonly}
<p>
Voici la liste des dépôts reconnus&nbsp;:
</p>
<form class="{className} form-floating" on:submit|preventDefault={submitWorkRepository}>
{#await remote_repos}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération de vos dépôts GitLab &hellip;</span>
</div>
{:then rrepos}
<select class="form-select col" disabled={readonly} bind:value={repo_used.uri}>
{#each rrepos as r (r.Id)}
<option value={r.ssh_url_to_repo}>{r.path_with_namespace}</option>
{/each}
</select>
<label>Dépôt GitLab pour ce travail&nbsp;:</label>
<button
type="submit"
class="mt-2 btn btn-primary"
disable={submitInProgress || readonly || !repo_used || !repo_used.uri}
>
Utiliser ce dépôt
</button>
{:catch err}
<div class="text-danger">
{err.message} Veuillez réessyer dans quelques instants&hellip;<br>Si le problème persiste, contactez votre professeur.
</div>
{/await}
<button
type="button"
class="mt-2 btn btn-info"
title="Rafraîchir la liste des dépôts"
on:click={() => remote_repos = getRemoteRepositories(user?user.id:null)}
>
<i class="bi bi-arrow-clockwise"></i>
</button>
</form>
{/if}
{/await}

View file

@ -0,0 +1,41 @@
import { writable } from 'svelte/store';
function createToastsStore() {
const { subscribe, set, update } = writable({toasts: []});
const addToast = (o) => {
o.timestamp = new Date();
o.close = () => {
update((i) => {
i.toasts = i.toasts.filter((j) => {
return !(j.title === o.title && j.msg === o.msg && j.timestamp === o.timestamp)
});
return i;
});
}
update((i) => {
i.toasts.unshift(o);
return i;
});
o.cancel = setTimeout(o.close, o.dismiss?o.dismiss:5000);
};
const addErrorToast = (o) => {
if (!o.title) o.title = 'Une erreur est survenue !';
if (!o.color) o.color = 'danger';
return addToast(o);
};
return {
subscribe,
addToast,
addErrorToast,
};
}
export const ToastsStore = createToastsStore();

27
ui/src/lib/stores/user.js Normal file
View file

@ -0,0 +1,27 @@
import { writable } from 'svelte/store';
function createUserStore() {
const { subscribe, set, update } = writable(undefined);
return {
subscribe,
set: (auth) => {
update((m) => auth);
},
update: (res_auth, cb=null) => {
if (res_auth.status === 200) {
res_auth.json().then((auth) => {
update((m) => (Object.assign(m?m:{}, auth)));
if (cb) {
cb(my);
}
});
} else if (res_auth.status >= 400 && res_auth.status < 500) {
update((m) => (null));
}
},
};
}
export const user = createUserStore();