Able to make corrections
This commit is contained in:
parent
6f958bdab4
commit
d77e6e2eac
22 changed files with 1085 additions and 80 deletions
47
atsebayt/src/components/Correction.svelte
Normal file
47
atsebayt/src/components/Correction.svelte
Normal 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…</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}
|
||||||
112
atsebayt/src/components/CorrectionReference.svelte
Normal file
112
atsebayt/src/components/CorrectionReference.svelte
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<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 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} %
|
||||||
|
{: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>
|
||||||
|
</div>
|
||||||
124
atsebayt/src/components/CorrectionResponseFooter.svelte
Normal file
124
atsebayt/src/components/CorrectionResponseFooter.svelte
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
<script>
|
||||||
|
import { user } from '../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} %
|
||||||
|
</strong>
|
||||||
|
<div class="col">
|
||||||
|
{my_correction.score_explaination}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
130
atsebayt/src/components/CorrectionResponses.svelte
Normal file
130
atsebayt/src/components/CorrectionResponses.svelte
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
<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 hilights = "";
|
||||||
|
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) && (!filter || r.value.match(filter)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyCorrections() {
|
||||||
|
for (const r of filteredResponses) {
|
||||||
|
const my_correction = { };
|
||||||
|
|
||||||
|
for (const tpl of templates) {
|
||||||
|
if (!tpl.regexp) continue;
|
||||||
|
|
||||||
|
if (r.value.match(tpl.regexp)) {
|
||||||
|
my_correction[tpl.id] = true;
|
||||||
|
} else {
|
||||||
|
my_correction[tpl.id] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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…</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}
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import QuestionProposals from '../components/QuestionProposals.svelte';
|
import QuestionHeader from './QuestionHeader.svelte';
|
||||||
|
import QuestionProposals from './QuestionProposals.svelte';
|
||||||
|
import ResponseCorrected from './ResponseCorrected.svelte';
|
||||||
import { user } from '../stores/user';
|
import { user } from '../stores/user';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
export let question;
|
export let question;
|
||||||
export let qid;
|
export let qid;
|
||||||
export let response_history = null;
|
export let response_history = null;
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
|
export let survey = null;
|
||||||
export let value = "";
|
export let value = "";
|
||||||
|
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
|
|
@ -28,8 +32,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card my-3 {className}">
|
<div class="card my-3 {className}">
|
||||||
<div class="card-header">
|
<QuestionHeader
|
||||||
{#if $user.is_admin}
|
bind:question={question}
|
||||||
|
{qid}
|
||||||
|
{edit}
|
||||||
|
>
|
||||||
|
{#if $user && $user.is_admin}
|
||||||
<button class="btn btn-sm btn-danger ms-1 float-end" on:click={() => dispatch('delete')}>
|
<button class="btn btn-sm btn-danger ms-1 float-end" on:click={() => dispatch('delete')}>
|
||||||
<i class="bi bi-trash-fill"></i>
|
<i class="bi bi-trash-fill"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -43,34 +51,7 @@
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
</QuestionHeader>
|
||||||
{#if edit}
|
|
||||||
<div class="card-title row">
|
|
||||||
<label for="q{qid}title" class="col-auto col-form-label font-weight-bold">Titre :</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">{qid + 1}. {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="int">Entier</option>
|
|
||||||
<option value="ucq">QCU</option>
|
|
||||||
<option value="mcq">QCM</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}
|
|
||||||
<p class="card-text mt-2">{@html question.description}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{#if false && response_history}
|
{#if false && response_history}
|
||||||
<div class="d-flex justify-content-end mb-2">
|
<div class="d-flex justify-content-end mb-2">
|
||||||
|
|
@ -132,6 +113,13 @@
|
||||||
<textarea class="form-control" rows="6" bind:value={value} placeholder={question.placeholder}></textarea>
|
<textarea class="form-control" rows="6" bind:value={value} placeholder={question.placeholder}></textarea>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if survey.corrected}
|
||||||
|
<ResponseCorrected
|
||||||
|
response={response_history}
|
||||||
|
{survey}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if false}
|
{#if false}
|
||||||
<div ng-controller="ProposalsController" ng-if="question.kind == 'ucq' || question.kind == 'mcq'">
|
<div ng-controller="ProposalsController" ng-if="question.kind == 'ucq' || question.kind == 'mcq'">
|
||||||
<div class="form-group form-check" ng-if="!question.edit && question.kind == 'mcq'" ng-repeat="proposal in proposals">
|
<div class="form-group form-check" ng-if="!question.edit && question.kind == 'mcq'" ng-repeat="proposal in proposals">
|
||||||
|
|
|
||||||
44
atsebayt/src/components/QuestionHeader.svelte
Normal file
44
atsebayt/src/components/QuestionHeader.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import { user } from '../stores/user';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
export let question = null;
|
||||||
|
export let qid = null;
|
||||||
|
export let edit = 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 :</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="int">Entier</option>
|
||||||
|
<option value="ucq">QCU</option>
|
||||||
|
<option value="mcq">QCM</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}
|
||||||
|
<p class="card-text mt-2">{@html question.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
export let proposals = [];
|
export let proposals = [];
|
||||||
export let kind = 'mcq';
|
export let kind = 'mcq';
|
||||||
|
export let prefixid = '';
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let id_question = 0;
|
export let id_question = 0;
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
let valueCheck = [];
|
let valueCheck = [];
|
||||||
$: {
|
$: {
|
||||||
console.log(value);
|
|
||||||
if (value) {
|
if (value) {
|
||||||
valueCheck = value.split(',');
|
valueCheck = value.split(',');
|
||||||
}
|
}
|
||||||
|
|
@ -31,10 +31,10 @@
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
name={'proposal' + proposal.id_question}
|
name={prefixid + 'proposal' + proposal.id_question}
|
||||||
id={'p' + proposal.id}
|
id={prefixid + 'p' + proposal.id}
|
||||||
bind:group={valueCheck}
|
bind:group={valueCheck}
|
||||||
value={String(proposal.id)}
|
value={proposal.id.toString()}
|
||||||
on:change={() => { value = valueCheck.join(',')}}
|
on:change={() => { value = valueCheck.join(',')}}
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -42,10 +42,10 @@
|
||||||
type="radio"
|
type="radio"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
name={'proposal' + proposal.id_question}
|
name={prefixid + 'proposal' + proposal.id_question}
|
||||||
id={'p' + proposal.id}
|
id={prefixid + 'p' + proposal.id}
|
||||||
bind:group={value}
|
bind:group={value}
|
||||||
value={String(proposal.id)}
|
value={proposal.id.toString()}
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if edit}
|
{#if edit}
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
{:else}
|
{:else}
|
||||||
<label
|
<label
|
||||||
class="form-check-label"
|
class="form-check-label"
|
||||||
for={'p' + proposal.id}
|
for={prefixid + 'p' + proposal.id}
|
||||||
>
|
>
|
||||||
{proposal.label}
|
{proposal.label}
|
||||||
</label>
|
</label>
|
||||||
|
|
|
||||||
52
atsebayt/src/components/ResponseCorrected.svelte
Normal file
52
atsebayt/src/components/ResponseCorrected.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
export let response = null;
|
||||||
|
export let survey = null;
|
||||||
|
</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} %, ce qui correspond à {Math.trunc(response.score*10/5)/10}/20."
|
||||||
|
>
|
||||||
|
{response.score} %
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{#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 !</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é ?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
<tr on:click={e => goto(`surveys/${survey.id}`)}>
|
<tr on:click={e => goto($user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
|
||||||
<td>
|
<td>
|
||||||
{survey.title}
|
{survey.title}
|
||||||
<SurveyBadge {survey} class="float-end" />
|
<SurveyBadge {survey} class="float-end" />
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
<form class="mb-5" on:submit|preventDefault={submitAnswers}>
|
<form class="mb-5" on:submit|preventDefault={submitAnswers}>
|
||||||
{#each questions as question, qid (question.id)}
|
{#each questions as question, qid (question.id)}
|
||||||
<QuestionForm
|
<QuestionForm
|
||||||
|
{survey}
|
||||||
qid={qid}
|
qid={qid}
|
||||||
question={question}
|
question={question}
|
||||||
response_history={responses[question.id]}
|
response_history={responses[question.id]}
|
||||||
|
|
@ -86,12 +87,14 @@
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<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)}>
|
<button type="submit" class="btn btn-primary" disabled={submitInProgress || (survey.isFinished() && !$user.is_admin)}>
|
||||||
{#if submitInProgress}
|
{#if submitInProgress}
|
||||||
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
||||||
{/if}
|
{/if}
|
||||||
Soumettre les réponses
|
Soumettre les réponses
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
{#if $user && $user.is_admin}
|
{#if $user && $user.is_admin}
|
||||||
<button type="button" class="btn btn-info" on:click={addQuestion}>
|
<button type="button" class="btn btn-info" on:click={addQuestion}>
|
||||||
Ajouter une question
|
Ajouter une question
|
||||||
|
|
|
||||||
88
atsebayt/src/lib/correctionTemplates.js
Normal file
88
atsebayt/src/lib/correctionTemplates.js
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
export class CorrectionTemplate {
|
||||||
|
constructor(res) {
|
||||||
|
if (res) {
|
||||||
|
this.update(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update({ id, id_question, label, regexp, score, score_explaination }) {
|
||||||
|
this.id = id;
|
||||||
|
this.id_question = id_question;
|
||||||
|
this.regexp = regexp;
|
||||||
|
this.label = label;
|
||||||
|
this.score = score;
|
||||||
|
this.score_explaination = score_explaination;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCorrections() {
|
||||||
|
if (this.id) {
|
||||||
|
const res = await fetch(`api/questions/${this.id_question}/corrections/${this.id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
});
|
||||||
|
if (res.status == 200) {
|
||||||
|
return await res.json();
|
||||||
|
} else {
|
||||||
|
throw new Error((await res.json()).errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const res = await fetch(this.id?`api/questions/${this.id_question}/corrections/${this.id}`:`api/questions/${this.id_question}/corrections`, {
|
||||||
|
method: this.id?'PUT':'POST',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
|||||||