Able to make corrections
This commit is contained in:
parent
df2da4221e
commit
11c49bcb34
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>
|
||||
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';
|
||||
|
||||
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 survey = null;
|
||||
export let value = "";
|
||||
|
||||
export let edit = false;
|
||||
@ -28,8 +32,12 @@
|
||||
</script>
|
||||
|
||||
<div class="card my-3 {className}">
|
||||
<div class="card-header">
|
||||
{#if $user.is_admin}
|
||||
<QuestionHeader
|
||||
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')}>
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
@ -43,34 +51,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#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>
|
||||
</QuestionHeader>
|
||||
<div class="card-body">
|
||||
{#if false && response_history}
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
{#if survey.corrected}
|
||||
<ResponseCorrected
|
||||
response={response_history}
|
||||
{survey}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if false}
|
||||
<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">
|
||||
|
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 proposals = [];
|
||||
export let kind = 'mcq';
|
||||
export let prefixid = '';
|
||||
export let readonly = false;
|
||||
export let id_question = 0;
|
||||
export let value;
|
||||
|
||||
let valueCheck = [];
|
||||
$: {
|
||||
console.log(value);
|
||||
if (value) {
|
||||
valueCheck = value.split(',');
|
||||
}
|
||||
@ -31,10 +31,10 @@
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
disabled={readonly}
|
||||
name={'proposal' + proposal.id_question}
|
||||
id={'p' + proposal.id}
|
||||
name={prefixid + 'proposal' + proposal.id_question}
|
||||
id={prefixid + 'p' + proposal.id}
|
||||
bind:group={valueCheck}
|
||||
value={String(proposal.id)}
|
||||
value={proposal.id.toString()}
|
||||
on:change={() => { value = valueCheck.join(',')}}
|
||||
>
|
||||
{:else}
|
||||
@ -42,10 +42,10 @@
|
||||
type="radio"
|
||||
class="form-check-input"
|
||||
disabled={readonly}
|
||||
name={'proposal' + proposal.id_question}
|
||||
id={'p' + proposal.id}
|
||||
name={prefixid + 'proposal' + proposal.id_question}
|
||||
id={prefixid + 'p' + proposal.id}
|
||||
bind:group={value}
|
||||
value={String(proposal.id)}
|
||||
value={proposal.id.toString()}
|
||||
>
|
||||
{/if}
|
||||
{#if edit}
|
||||
@ -80,7 +80,7 @@
|
||||
{:else}
|
||||
<label
|
||||
class="form-check-label"
|
||||
for={'p' + proposal.id}
|
||||
for={prefixid + 'p' + proposal.id}
|
||||
>
|
||||
{proposal.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>
|
||||
</tr>
|
||||
{/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>
|
||||
{survey.title}
|
||||
<SurveyBadge {survey} class="float-end" />
|
||||
|
@ -68,6 +68,7 @@
|
||||
<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]}
|
||||
@ -86,12 +87,14 @@
|
||||
{/each}
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<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 !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
|
||||
|
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'},
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
if (res.status == 200) {
|
||||
const data = await res.json();
|
||||
this.update(data);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (this.id) {
|
||||
const res = await fetch(`api/questions/${this.id_question}/corrections/${this.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {'Accept': 'application/json'},
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCorrectionTemplates(qid) {
|
||||
const res = await fetch(`api/questions/${qid}/corrections`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
const data = await res.json();
|
||||
if (data === null) {
|
||||
return [];
|
||||
} else {
|
||||
return (data).map((c) => new CorrectionTemplate(c))
|
||||
}
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function autoCorrection(id_user, my_tpls) {
|
||||
const res = await fetch(`api/users/${id_user}/corrections`, {
|
||||
method: 'PUT',
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(my_tpls),
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { Response } from './response';
|
||||
|
||||
export class QuestionProposal {
|
||||
constructor(res) {
|
||||
if (res) {
|
||||
@ -72,6 +74,18 @@ export class Question {
|
||||
}
|
||||
}
|
||||
|
||||
async getResponses() {
|
||||
const res = await fetch(`api/surveys/${this.id_survey}/questions/${this.id}/responses`, {
|
||||
method: 'GET',
|
||||
headers: {'Accept': 'application/json'},
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).map((r) => new Response(r))
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
const res = await fetch(this.id?`api/questions/${this.id}`:`api/surveys/${this.id_survey}/questions`, {
|
||||
method: this.id?'PUT':'POST',
|
||||
@ -102,6 +116,15 @@ export class Question {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQuestion(qid) {
|
||||
const res = await fetch(`api/questions/${qid}`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return new Question(await res.json());
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQuestions(sid) {
|
||||
const res = await fetch(`api/surveys/${sid}/questions`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
|
34
atsebayt/src/lib/response.js
Normal file
34
atsebayt/src/lib/response.js
Normal file
@ -0,0 +1,34 @@
|
||||
export class Response {
|
||||
constructor(res) {
|
||||
if (res) {
|
||||
this.update(res);
|
||||
}
|
||||
}
|
||||
|
||||
update({ id, id_question, id_user, value, time_submit, score, score_explaination, id_corrector, time_scored }) {
|
||||
this.id = id;
|
||||
this.id_question = id_question;
|
||||
this.id_user = id_user;
|
||||
this.value = value;
|
||||
this.time_submit = time_submit;
|
||||
this.score = score;
|
||||
this.score_explaination = score_explaination;
|
||||
this.id_corrector = id_corrector;
|
||||
this.time_scored = time_scored;
|
||||
}
|
||||
|
||||
async save() {
|
||||
const res = await fetch(`api/questions/${this.id_question}/responses/${this.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
if (res.status == 200) {
|
||||
const data = await res.json();
|
||||
this.update(data);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
}
|
@ -111,7 +111,6 @@ class Survey {
|
||||
}
|
||||
|
||||
async delete() {
|
||||
console.log("delete", this.id)
|
||||
if (this.id) {
|
||||
const res = await fetch(`api/surveys/${this.id}`, {
|
||||
method: 'DELETE',
|
||||
|
@ -59,6 +59,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>SRS: MCQ and others courses related stuff</title>
|
||||
</svelte:head>
|
||||
|
||||
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href=".">
|
||||
|
39
atsebayt/src/routes/surveys/[sid]/__layout.svelte
Normal file
39
atsebayt/src/routes/surveys/[sid]/__layout.svelte
Normal file
@ -0,0 +1,39 @@
|
||||
<script context="module">
|
||||
import { getSurvey } from '../../../lib/surveys';
|
||||
|
||||
export async function load({ params, stuff }) {
|
||||
const survey = getSurvey(params.sid);
|
||||
|
||||
return {
|
||||
props: {
|
||||
survey,
|
||||
},
|
||||
stuff: {
|
||||
...stuff,
|
||||
survey,
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
export let survey;
|
||||
</script>
|
||||
|
||||
{#await survey}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement du questionnaire …</span>
|
||||
</div>
|
||||
{:then}
|
||||
<slot></slot>
|
||||
{:catch error}
|
||||
<div class="text-center">
|
||||
<h2>
|
||||
<a href="surveys/" class="text-muted" style="text-decoration: none"><</a>
|
||||
Questionnaire introuvable
|
||||
</h2>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/await}
|
@ -1,32 +1,29 @@
|
||||
<script context="module">
|
||||
export async function load({ params }) {
|
||||
import { getSurvey } from '../../../lib/surveys';
|
||||
|
||||
export async function load({ params, stuff }) {
|
||||
return {
|
||||
props: {
|
||||
sid: params.sid,
|
||||
}
|
||||
surveyP: stuff.survey,
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { user } from '../../stores/user';
|
||||
import SurveyAdmin from '../../components/SurveyAdmin.svelte';
|
||||
import SurveyBadge from '../../components/SurveyBadge.svelte';
|
||||
import SurveyQuestions from '../../components/SurveyQuestions.svelte';
|
||||
import { getSurvey } from '../../lib/surveys';
|
||||
import { getQuestions } from '../../lib/questions';
|
||||
import { user } from '../../../stores/user';
|
||||
import SurveyAdmin from '../../../components/SurveyAdmin.svelte';
|
||||
import SurveyBadge from '../../../components/SurveyBadge.svelte';
|
||||
import SurveyQuestions from '../../../components/SurveyQuestions.svelte';
|
||||
import { getSurvey } from '../../../lib/surveys';
|
||||
import { getQuestions } from '../../../lib/questions';
|
||||
|
||||
export let sid;
|
||||
export let surveyP;
|
||||
|
||||
let edit = false;
|
||||
</script>
|
||||
|
||||
{#await getSurvey(sid)}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement du questionnaire …</span>
|
||||
</div>
|
||||
{:then survey}
|
||||
{#await surveyP then survey}
|
||||
{#if $user && $user.is_admin}
|
||||
<button class="btn btn-primary ms-1 float-end" on:click={() => { edit = !edit; } } title="Éditer" ng-if=" && !edit"><i class="bi bi-pencil"></i></button>
|
||||
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses" ng-if="user.is_admin"><i class="bi bi-files"></i></a>
|
||||
@ -51,12 +48,4 @@
|
||||
{:then questions}
|
||||
<SurveyQuestions {survey} {questions} />
|
||||
{/await}
|
||||
{:catch error}
|
||||
<div class="text-center">
|
||||
<h2>
|
||||
<a href="surveys/" class="text-muted" style="text-decoration: none"><</a>
|
||||
Questionnaire introuvable
|
||||
</h2>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/await}
|
156
atsebayt/src/routes/surveys/[sid]/responses/[rid].svelte
Normal file
156
atsebayt/src/routes/surveys/[sid]/responses/[rid].svelte
Normal file
@ -0,0 +1,156 @@
|
||||
<script context="module">
|
||||
import { getSurvey } from '../../../../lib/surveys';
|
||||
|
||||
export async function load({ params, stuff }) {
|
||||
return {
|
||||
props: {
|
||||
surveyP: stuff.survey,
|
||||
rid: params.rid,
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Correction from '../../../../components/Correction.svelte';
|
||||
import CorrectionReference from '../../../../components/CorrectionReference.svelte';
|
||||
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
|
||||
import QuestionHeader from '../../../../components/QuestionHeader.svelte';
|
||||
import { getCorrectionTemplates } from '../../../../lib/correctionTemplates';
|
||||
import { getQuestion } from '../../../../lib/questions';
|
||||
|
||||
export let surveyP;
|
||||
export let rid;
|
||||
|
||||
let showResponses = false;
|
||||
let showStudent = false;
|
||||
let notCorrected = false;
|
||||
let nb_responses = 0;
|
||||
|
||||
let child;
|
||||
let waitApply = false;
|
||||
let ctpls = getCorrectionTemplates(rid);
|
||||
let filter = "";
|
||||
|
||||
let cts = { };
|
||||
ctpls.then((ctpls) => {
|
||||
for (const tpl of ctpls) {
|
||||
cts[tpl.id] = { };
|
||||
tpl.getCorrections().then((c) => {
|
||||
if (c) {
|
||||
for (const d of c) {
|
||||
cts[tpl.id][d.id_user] = d;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
cts = cts;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#await surveyP then survey}
|
||||
{#await getQuestion(rid)}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement de la question…</span>
|
||||
</div>
|
||||
{:then question}
|
||||
{#await ctpls}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement de la question…</span>
|
||||
</div>
|
||||
{:then correctionTemplates}
|
||||
<div class="float-end">
|
||||
<input
|
||||
class="form-control"
|
||||
placeholder="filtre"
|
||||
bind:value={filter}
|
||||
>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="surveys/{survey.id}/responses" class="text-muted" style="text-decoration: none"><</a>
|
||||
{survey.title}
|
||||
<small class="text-muted">Corrections</small>
|
||||
</h2>
|
||||
<SurveyBadge class="ms-2" {survey} />
|
||||
</div>
|
||||
|
||||
<div class="card sticky-top">
|
||||
<QuestionHeader
|
||||
{question}
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm btn-link float-start"
|
||||
on:click={() => showResponses = !showResponses}
|
||||
>
|
||||
<i
|
||||
class="bi"
|
||||
class:bi-chevron-right={!showResponses}
|
||||
class:bi-chevron-down={showResponses}
|
||||
></i>
|
||||
</button>
|
||||
{#if showResponses}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-success float-end ms-3 me-1"
|
||||
title="Appliquer les corrections par regexp"
|
||||
on:click={() => {waitApply = true; child.applyCorrections().then(() => { waitApply = false; })} }
|
||||
disabled={waitApply}
|
||||
>
|
||||
{#if waitApply}
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
{:else}
|
||||
<i class="bi bi-check-all"></i>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm float-end mx-1"
|
||||
class:btn-outline-info={!showStudent}
|
||||
class:btn-info={showStudent}
|
||||
on:click={() => showStudent = !showStudent}
|
||||
title="Afficher les étudiants"
|
||||
>
|
||||
<i class="bi bi-people"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm float-end mx-1"
|
||||
class:btn-outline-info={!notCorrected}
|
||||
class:btn-info={notCorrected}
|
||||
on:click={() => notCorrected = !notCorrected}
|
||||
title="Afficher les réponses corrigées"
|
||||
>
|
||||
<i class="bi bi-files"></i>
|
||||
</button>
|
||||
</QuestionHeader>
|
||||
{#if showResponses}
|
||||
<CorrectionReference
|
||||
class="card-body"
|
||||
{cts}
|
||||
bind:filter={filter}
|
||||
{nb_responses}
|
||||
{question}
|
||||
templates={correctionTemplates}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Correction
|
||||
{cts}
|
||||
{filter}
|
||||
{question}
|
||||
{showStudent}
|
||||
{notCorrected}
|
||||
bind:child={child}
|
||||
templates={correctionTemplates}
|
||||
on:nb_responses={(v) => { nb_responses = v.detail; } }
|
||||
/>
|
||||
{/await}
|
||||
{/await}
|
||||
{/await}
|
||||
|
||||
<div class="mb-5"></div>
|
85
atsebayt/src/routes/surveys/[sid]/responses/index.svelte
Normal file
85
atsebayt/src/routes/surveys/[sid]/responses/index.svelte
Normal file
@ -0,0 +1,85 @@
|
||||
<script context="module">
|
||||
import { getSurvey } from '../../../../lib/surveys';
|
||||
|
||||
export async function load({ params, stuff }) {
|
||||
return {
|
||||
props: {
|
||||
surveyP: stuff.survey,
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
|
||||
import SurveyQuestions from '../../../../components/SurveyQuestions.svelte';
|
||||
import { getSurvey } from '../../../../lib/surveys';
|
||||
import { getQuestions } from '../../../../lib/questions';
|
||||
|
||||
export let surveyP;
|
||||
</script>
|
||||
|
||||
{#await surveyP then survey}
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="surveys/{survey.id}" class="text-muted" style="text-decoration: none"><</a>
|
||||
{survey.title}
|
||||
<small class="text-muted">Corrections</small>
|
||||
</h2>
|
||||
<SurveyBadge class="ms-2" {survey} />
|
||||
</div>
|
||||
|
||||
{#await getQuestions(survey.id)}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement des questions …</span>
|
||||
</div>
|
||||
{:then questions}
|
||||
<div class="card mt-3 mb-5">
|
||||
<table class="table table-hover table-striped mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Question</th>
|
||||
<th>Réponses</th>
|
||||
<th>Moyenne</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-controller="SurveyGradesController">
|
||||
{#each questions as question (question.id)}
|
||||
<tr ng-click="showResponses()" ng-controller="ResponsesController">
|
||||
<td><a href="surveys/{survey.id}/responses/{question.id}">{question.title}</a></td>
|
||||
{#await question.getResponses()}
|
||||
<td colspan="2" class="text-center">
|
||||
<div class="spinner-border mx-3" role="status"></div>
|
||||
<span>Chargement …</span>
|
||||
</td>
|
||||
{:then responses}
|
||||
<td>
|
||||
{#if responses}
|
||||
{responses.filter((r) => !r.time_scored).length} /
|
||||
{responses.length}
|
||||
{:else}
|
||||
0
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if responses && responses.filter((r) => r.time_scored).length}
|
||||
{responses.reduce((p, c) => (p + c.score?c.score:0), 0)/responses.filter((r) => r.time_scored).length}
|
||||
{:else}
|
||||
-- %
|
||||
{/if}
|
||||
</td>
|
||||
{/await}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th colspan="2">Moyenne</th>
|
||||
<th><!--{mean}--> %</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{/await}
|
||||
{/await}
|
@ -23,7 +23,7 @@ func init() {
|
||||
return APIErrorResponse{err: err}
|
||||
}
|
||||
|
||||
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.Score, new.ScoreExplaination))
|
||||
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination))
|
||||
}), adminRestricted))
|
||||
|
||||
router.GET("/api/surveys/:sid/questions/:qid/corrections/:cid", apiHandler(correctionHandler(
|
||||
@ -51,6 +51,48 @@ func init() {
|
||||
return formatApiResponse(ct.Delete())
|
||||
}), adminRestricted))
|
||||
|
||||
router.GET("/api/questions/:qid/corrections", apiHandler(questionHandler(
|
||||
func(q Question, _ []byte) HTTPResponse {
|
||||
if cts, err := q.GetCorrectionTemplates(); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
} else {
|
||||
return APIResponse{cts}
|
||||
}
|
||||
}), adminRestricted))
|
||||
router.POST("/api/questions/:qid/corrections", apiHandler(questionHandler(func(q Question, body []byte) HTTPResponse {
|
||||
var new CorrectionTemplate
|
||||
if err := json.Unmarshal(body, &new); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
}
|
||||
|
||||
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination))
|
||||
}), adminRestricted))
|
||||
|
||||
router.GET("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(
|
||||
func(ct CorrectionTemplate, _ []byte) HTTPResponse {
|
||||
if users, err := ct.GetUserCorrected(); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
} else {
|
||||
return APIResponse{users}
|
||||
}
|
||||
}), adminRestricted))
|
||||
router.PUT("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(current CorrectionTemplate, body []byte) HTTPResponse {
|
||||
var new CorrectionTemplate
|
||||
if err := json.Unmarshal(body, &new); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
}
|
||||
|
||||
new.Id = current.Id
|
||||
if err := new.Update(); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
} else {
|
||||
return APIResponse{new}
|
||||
}
|
||||
}), adminRestricted))
|
||||
router.DELETE("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(ct CorrectionTemplate, body []byte) HTTPResponse {
|
||||
return formatApiResponse(ct.Delete())
|
||||
}), adminRestricted))
|
||||
|
||||
router.GET("/api/users/:uid/questions/:qid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
|
||||
return userHandler(func(u User, _ []byte) HTTPResponse {
|
||||
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil {
|
||||
@ -75,6 +117,14 @@ func init() {
|
||||
|
||||
return formatApiResponse(u.NewCorrection(new.IdTemplate))
|
||||
}), adminRestricted))
|
||||
router.PUT("/api/users/:uid/corrections", apiHandler(userHandler(func(u User, body []byte) HTTPResponse {
|
||||
var new map[int64]bool
|
||||
if err := json.Unmarshal(body, &new); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
}
|
||||
|
||||
return formatApiResponse(u.EraseCorrections(new))
|
||||
}), adminRestricted))
|
||||
router.DELETE("/api/users/:uid/corrections/:cid", apiHandler(userCorrectionHandler(func(u User, uc UserCorrection, body []byte) HTTPResponse {
|
||||
return formatApiResponse(uc.Delete(u))
|
||||
}), adminRestricted))
|
||||
@ -112,19 +162,20 @@ type CorrectionTemplate struct {
|
||||
Id int64 `json:"id"`
|
||||
IdQuestion int64 `json:"id_question"`
|
||||
Label string `json:"label"`
|
||||
RegExp string `json:"regexp"`
|
||||
Score int `json:"score"`
|
||||
ScoreExplaination string `json:"score_explaination,omitempty"`
|
||||
}
|
||||
|
||||
func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error) {
|
||||
if rows, errr := DBQuery("SELECT id_template, id_question, label, score, score_explanation FROM correction_templates WHERE id_question=?", q.Id); errr != nil {
|
||||
if rows, errr := DBQuery("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_question=?", q.Id); errr != nil {
|
||||
return nil, errr
|
||||
} else {
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var c CorrectionTemplate
|
||||
if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination); err != nil {
|
||||
if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination); err != nil {
|
||||
return
|
||||
}
|
||||
ct = append(ct, c)
|
||||
@ -138,27 +189,27 @@ func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error)
|
||||
}
|
||||
|
||||
func (q *Question) GetCorrectionTemplate(id int) (c CorrectionTemplate, err error) {
|
||||
err = DBQueryRow("SELECT id_template, id_question, label, score, score_explanation FROM correction_templates WHERE id_question=? AND id_template=?", q.Id, id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination)
|
||||
err = DBQueryRow("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_question=? AND id_template=?", q.Id, id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination)
|
||||
return
|
||||
}
|
||||
|
||||
func GetCorrectionTemplate(id int64) (c CorrectionTemplate, err error) {
|
||||
err = DBQueryRow("SELECT id_template, id_question, label, score, score_explanation FROM correction_templates WHERE id_template=?", id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination)
|
||||
err = DBQueryRow("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_template=?", id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination)
|
||||
return
|
||||
}
|
||||
|
||||
func (q *Question) NewCorrectionTemplate(label string, score int, score_explaination string) (CorrectionTemplate, error) {
|
||||
if res, err := DBExec("INSERT INTO correction_templates (id_question, label, score, score_explanation) VALUES (?, ?, ?, ?)", q.Id, label, score, score_explaination); err != nil {
|
||||
func (q *Question) NewCorrectionTemplate(label string, regexp string, score int, score_explaination string) (CorrectionTemplate, error) {
|
||||
if res, err := DBExec("INSERT INTO correction_templates (id_question, label, re, score, score_explanation) VALUES (?, ?, ?, ?, ?)", q.Id, label, regexp, score, score_explaination); err != nil {
|
||||
return CorrectionTemplate{}, err
|
||||
} else if cid, err := res.LastInsertId(); err != nil {
|
||||
return CorrectionTemplate{}, err
|
||||
} else {
|
||||
return CorrectionTemplate{cid, q.Id, label, score, score_explaination}, nil
|
||||
return CorrectionTemplate{cid, q.Id, label, regexp, score, score_explaination}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CorrectionTemplate) Update() error {
|
||||
_, err := DBExec("UPDATE correction_templates SET id_question = ?, label = ?, score = ?, score_explanation = ? WHERE id_template = ?", t.IdQuestion, t.Label, t.Score, t.ScoreExplaination, t.Id)
|
||||
_, err := DBExec("UPDATE correction_templates SET id_question = ?, label = ?, re = ?, score = ?, score_explanation = ? WHERE id_template = ?", t.IdQuestion, t.Label, t.RegExp, t.Score, t.ScoreExplaination, t.Id)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -269,6 +320,26 @@ func (u *User) NewCorrection(id int64) (*UserCorrectionSummary, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) EraseCorrections(ids map[int64]bool) (*UserCorrectionSummary, error) {
|
||||
var lastid int64
|
||||
|
||||
for id, st := range ids {
|
||||
lastid = id
|
||||
if st {
|
||||
DBExec("INSERT INTO student_corrected (id_user, id_template) VALUES (?, ?)", u.Id, id)
|
||||
} else {
|
||||
DBExec("DELETE FROM student_corrected WHERE id_user = ? AND id_template = ?", u.Id, id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ucs, err := u.ComputeScoreQuestion(lastid); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return ucs, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UserCorrection) Delete(u User) (*UserCorrectionSummary, error) {
|
||||
if res, err := DBExec("DELETE FROM student_corrected WHERE id_correction = ?", c.Id); err != nil {
|
||||
return nil, err
|
||||
|
1
db.go
1
db.go
@ -136,6 +136,7 @@ CREATE TABLE IF NOT EXISTS correction_templates(
|
||||
id_template INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||
id_question INTEGER NOT NULL,
|
||||
label VARCHAR(255) NOT NULL,
|
||||
re VARCHAR(255) NOT NULL,
|
||||
score INTEGER,
|
||||
score_explanation TEXT,
|
||||
FOREIGN KEY(id_question) REFERENCES survey_quests(id_question)
|
||||
|
16
responses.go
16
responses.go
@ -85,6 +85,22 @@ func init() {
|
||||
new.TimeScored = &now
|
||||
}
|
||||
|
||||
new.Id = current.Id
|
||||
new.IdUser = current.IdUser
|
||||
return formatApiResponse(new.Update())
|
||||
}), adminRestricted))
|
||||
router.PUT("/api/questions/:qid/responses/:rid", apiAuthHandler(responseAuthHandler(func(current Response, u *User, body []byte) HTTPResponse {
|
||||
var new Response
|
||||
if err := json.Unmarshal(body, &new); err != nil {
|
||||
return APIErrorResponse{err: err}
|
||||
}
|
||||
|
||||
if new.Score != nil && (current.Score == nil || *new.Score != *current.Score) {
|
||||
now := time.Now()
|
||||
new.IdCorrector = &u.Id
|
||||
new.TimeScored = &now
|
||||
}
|
||||
|
||||
new.Id = current.Id
|
||||
new.IdUser = current.IdUser
|
||||
return formatApiResponse(new.Update())
|
||||
|
Reference in New Issue
Block a user