Able to make corrections

This commit is contained in:
nemunaire 2022-02-28 10:52:27 +01:00
commit d77e6e2eac
22 changed files with 1085 additions and 80 deletions

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,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}&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>
</div>

View 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}&nbsp;%
</strong>
<div class="col">
{my_correction.score_explaination}
</div>
</div>
{/if}

View 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&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

@ -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&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">{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">

View 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&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="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>

View file

@ -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>

View 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}&nbsp;%, ce qui correspond à {Math.trunc(response.score*10/5)/10}/20."
>
{response.score}&nbsp;%
</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&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

@ -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" />

View file

@ -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

View 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'},