Compare commits

..

2 Commits

Author SHA1 Message Date
894358df20 Add toaster
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-01 13:17:57 +01:00
3ae644fe13 Live: Retrieve previous submission 2022-03-01 13:03:16 +01:00
11 changed files with 150 additions and 15 deletions

View File

@ -3,6 +3,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { getQuestions } from '../lib/questions'; import { getQuestions } from '../lib/questions';
import { ToastsStore } from '../stores/toasts';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let survey = null; export let survey = null;
@ -11,7 +12,9 @@
survey.save().then((response) => { survey.save().then((response) => {
dispatch('saved'); dispatch('saved');
}, (error) => { }, (error) => {
console.log(error) ToastsStore.addErrorToast({
msg: error.errmsg,
});
}) })
} }
@ -19,7 +22,9 @@
survey.delete().then((response) => { survey.delete().then((response) => {
goto(`surveys`); goto(`surveys`);
}, (error) => { }, (error) => {
console.log(error) ToastsStore.addErrorToast({
msg: error.errmsg,
});
}) })
} }
@ -27,7 +32,9 @@
survey.duplicate().then((response) => { survey.duplicate().then((response) => {
goto(`surveys/${response.id}`); goto(`surveys/${response.id}`);
}).catch((error) => { }).catch((error) => {
console.log(error) ToastsStore.addErrorToast({
msg: error.errmsg,
});
}) })
} }

View File

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

View File

@ -12,7 +12,7 @@
req_surveys.then((surveys) => { req_surveys.then((surveys) => {
for (const survey of surveys) { for (const survey of surveys) {
if (survey.direct) { if (survey.direct != null) {
direct = survey; direct = survey;
} }
} }
@ -39,7 +39,7 @@
{:then surveys} {:then surveys}
<tbody style="cursor: pointer;"> <tbody style="cursor: pointer;">
{#each surveys as survey, sid (survey.id)} {#each surveys as survey, sid (survey.id)}
{#if survey.shown && (!$user || (!$user.was_admin || $user.promo == survey.promo) || $user.is_admin)} {#if (survey.shown || survey.direct != null) && (!$user || (!$user.was_admin || $user.promo == survey.promo) || $user.is_admin)}
{#if $user && $user.is_admin && (sid == 0 || surveys[sid-1].promo != survey.promo)} {#if $user && $user.is_admin && (sid == 0 || surveys[sid-1].promo != survey.promo)}
<tr class="bg-info text-light"> <tr class="bg-info text-light">
<th colspan="5" class="fw-bold"> <th colspan="5" class="fw-bold">
@ -47,7 +47,7 @@
</th> </th>
</tr> </tr>
{/if} {/if}
<tr on:click={e => goto(survey.direct?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}> <tr on:click={e => goto(survey.direct != null ?`surveys/${survey.id}/live`:$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

@ -1,5 +1,6 @@
<script> <script>
import { user } from '../stores/user'; import { user } from '../stores/user';
import { ToastsStore } from '../stores/toasts';
import QuestionForm from '../components/QuestionForm.svelte'; import QuestionForm from '../components/QuestionForm.svelte';
import { Question } from '../lib/questions'; import { Question } from '../lib/questions';
@ -19,10 +20,16 @@
survey.submitAnswers(res, id_user).then((response) => { survey.submitAnswers(res, id_user).then((response) => {
submitInProgress = false; submitInProgress = false;
console.log("Vos réponses ont bien étés sauvegardées."); ToastsStore.addToast({
msg: "Vos réponses ont bien étés sauvegardées.",
color: "success",
title: "Questionnaire",
});
}, (error) => { }, (error) => {
submitInProgress = false; submitInProgress = false;
console.log("Une erreur s'est produite durant l'envoi de vos réponses : " + error + "<br>Veuillez réessayer dans quelques instants."); ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez réessayer dans quelques instants.",
});
}); });
} }

View File

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

View File

@ -74,6 +74,18 @@ export class Question {
} }
} }
async getMyResponse() {
const res = await fetch(`api/questions/${this.id}/response`, {
method: 'GET',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return new Response(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}
async getResponses() { async getResponses() {
const res = await fetch(`api/surveys/${this.id_survey}/questions/${this.id}/responses`, { const res = await fetch(`api/surveys/${this.id_survey}/questions/${this.id}/responses`, {
method: 'GET', method: 'GET',

View File

@ -42,6 +42,8 @@
</script> </script>
<script> <script>
import Toaster from '../components/Toaster.svelte';
export let rroute = ''; export let rroute = '';
function switchAdminMode() { function switchAdminMode() {
@ -140,3 +142,5 @@
<div class="container mt-3"> <div class="container mt-3">
<slot></slot> <slot></slot>
</div> </div>
<Toaster />

View File

@ -181,7 +181,7 @@
disabled={!current_question || !ws_up} disabled={!current_question || !ws_up}
on:click={() => { ws.send('{"action":"pause"}')} } on:click={() => { ws.send('{"action":"pause"}')} }
> >
Pause <i class="bi bi-pause-fill"></i>
</button> </button>
</th> </th>
</tr> </tr>
@ -213,7 +213,7 @@
disabled={question.id === current_question || !ws_up} disabled={question.id === current_question || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "question":' + question.id + '}')} } on:click={() => { ws.send('{"action":"new_question", "question":' + question.id + '}')} }
> >
Lancer cette question <i class="bi bi-play-fill"></i>
</button> </button>
</td> </td>
</tr> </tr>

View File

@ -11,6 +11,7 @@
<script> <script>
import { user } from '../../../stores/user'; import { user } from '../../../stores/user';
import { ToastsStore } from '../../../stores/toasts';
import SurveyBadge from '../../../components/SurveyBadge.svelte'; import SurveyBadge from '../../../components/SurveyBadge.svelte';
import QuestionForm from '../../../components/QuestionForm.svelte'; import QuestionForm from '../../../components/QuestionForm.svelte';
import { getQuestion } from '../../../lib/questions'; import { getQuestion } from '../../../lib/questions';
@ -25,6 +26,26 @@
let show_question = null; let show_question = null;
let value; let value;
let req_question;
let nosend = false;
function afterQUpdate(q) {
value = undefined;
if (q) {
q.getMyResponse().then((response) => {
if (response && response.value)
value = response.value;
})
}
}
$: {
if (show_question) {
req_question = getQuestion(show_question);
req_question.then(afterQUpdate);
}
}
function wsconnect() { function wsconnect() {
const ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`); const ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`);
@ -59,12 +80,14 @@
wsconnect(); wsconnect();
function sendValue() { function sendValue() {
if (show_question && value) { if (show_question && value && !nosend) {
survey.submitAnswers([{"id_question": show_question, "value": value}], $user.id_user).then((response) => { survey.submitAnswers([{"id_question": show_question, "value": value}], $user.id_user).then((response) => {
console.log("Vos réponses ont bien étés sauvegardées."); console.log("Vos réponses ont bien étés sauvegardées.");
}, (error) => { }, (error) => {
value = null; value = null;
console.log("Une erreur s'est produite durant l'envoi de vos réponses : " + error + "<br>Veuillez réessayer dans quelques instants."); ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez réessayer dans quelques instants.",
});
}); });
} }
} }
@ -91,8 +114,11 @@
<form on:submit|preventDefault={sendValue}> <form on:submit|preventDefault={sendValue}>
{#if show_question} {#if show_question}
{#await getQuestion(show_question)} {#await req_question}
Please wait <div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement d'une nouvelle question &hellip;</span>
</div>
{:then question} {:then question}
<QuestionForm <QuestionForm
qid={show_question} qid={show_question}
@ -104,6 +130,13 @@
<div class="progress-bar" role="progressbar" style="width: 25%"></div> <div class="progress-bar" role="progressbar" style="width: 25%"></div>
</div--> </div-->
</QuestionForm> </QuestionForm>
{#if question.kind != 'mcq' && question.kind != 'ucq'}
<button
class="btn btn-primary"
>
Soumettre la réponse
</button>
{/if}
{/await} {/await}
{:else if ws_up} {:else if ws_up}
<h2 class="text-center"> <h2 class="text-center">

View File

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

View File

@ -64,6 +64,10 @@ func init() {
func(s Survey, u *User, _ []byte) HTTPResponse { func(s Survey, u *User, _ []byte) HTTPResponse {
return formatApiResponse(s.GetMyResponses(u, s.Corrected)) return formatApiResponse(s.GetMyResponses(u, s.Corrected))
}), loggedUser)) }), loggedUser))
router.GET("/api/questions/:qid/response", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetMyResponse(u, false))
}), loggedUser))
router.GET("/api/users/:uid/surveys/:sid/responses", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { router.GET("/api/users/:uid/surveys/:sid/responses", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse { return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse { return userHandler(func(u User, _ []byte) HTTPResponse {
@ -207,6 +211,15 @@ func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response,
} }
} }
func (q *Question) GetMyResponse(u *User, showScore bool) (r Response, err error) {
err = DBQueryRow("SELECT R.id_response, R.id_question, R.id_user, R.answer, R.time_submit, R.score, R.score_explanation, R.id_corrector, R.time_scored FROM survey_responses R WHERE R.id_question=? AND R.id_user=? ORDER BY time_submit DESC LIMIT 1", q.Id, u.Id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored)
if !showScore {
r.Score = nil
r.ScoreExplaination = nil
}
return
}
func (q *Question) GetResponses() (responses []Response, err error) { func (q *Question) GetResponses() (responses []Response, err error) {
if rows, errr := DBQuery("SELECT id_response, id_question, S.id_user, answer, S.time_submit, score, score_explanation, id_corrector, time_scored FROM (SELECT id_user, MAX(time_submit) AS time_submit FROM survey_responses WHERE id_question=? GROUP BY id_user) R INNER JOIN survey_responses S ON S.id_user = R.id_user AND S.time_submit = R.time_submit AND S.id_question=?", q.Id, q.Id); errr != nil { if rows, errr := DBQuery("SELECT id_response, id_question, S.id_user, answer, S.time_submit, score, score_explanation, id_corrector, time_scored FROM (SELECT id_user, MAX(time_submit) AS time_submit FROM survey_responses WHERE id_question=? GROUP BY id_user) R INNER JOIN survey_responses S ON S.id_user = R.id_user AND S.time_submit = R.time_submit AND S.id_question=?", q.Id, q.Id); errr != nil {
return nil, errr return nil, errr