Working on live surveys
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
nemunaire 2022-02-28 19:00:30 +01:00
commit 0e5961c406
19 changed files with 1014 additions and 48 deletions

View file

@ -52,6 +52,7 @@
{/if}
{/if}
</QuestionHeader>
<slot></slot>
<div class="card-body">
{#if false && response_history}
<div class="d-flex justify-content-end mb-2">
@ -88,6 +89,7 @@
{proposals}
readonly
bind:value={value}
on:change={() => { dispatch("change"); }}
/>
{/await}
{/if}
@ -103,17 +105,30 @@
{proposals}
{readonly}
bind:value={value}
on:change={() => { dispatch("change"); }}
/>
{/await}
{:else if readonly}
<p class="card-text alert alert-secondary" style="white-space: pre-line">{value}</p>
{:else if question.kind == 'int'}
<input class="ml-5 col-sm-2 form-control" type="number" bind:value={value} placeholder={question.placeholder}>
<input
class="ml-5 col-sm-2 form-control"
type="number"
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
>
{:else}
<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}
on:change={() => { dispatch("change"); }}
></textarea>
{/if}
{#if survey.corrected}
{#if survey && survey.corrected}
<ResponseCorrected
response={response_history}
{survey}

View file

@ -1,21 +0,0 @@
<script>
export let proposal = null;
export let kind = 'mcq';
export let value;
let inputType = 'checkbox';
$: {
switch(kind) {
case 'mcq':
inputType = 'checkbox';
break;
default:
inputType = 'radio';
}
}
</script>
<input
type={inputType}
bind:value={value}
{JSON.stringify(proposal)}

View file

@ -1,4 +1,6 @@
<script>
import { createEventDispatcher } from 'svelte';
import { QuestionProposal } from '../lib/questions';
export let edit = false;
@ -16,6 +18,8 @@
}
}
const dispatch = createEventDispatcher();
function addProposal() {
const p = new QuestionProposal();
p.id_question = id_question;
@ -35,7 +39,7 @@
id={prefixid + 'p' + proposal.id}
bind:group={valueCheck}
value={proposal.id.toString()}
on:change={() => { value = valueCheck.join(',')}}
on:change={() => { value = valueCheck.join(','); dispatch("change"); }}
>
{:else}
<input
@ -46,6 +50,7 @@
id={prefixid + 'p' + proposal.id}
bind:group={value}
value={proposal.id.toString()}
on:change={() => { dispatch("change"); }}
>
{/if}
{#if edit}

View file

@ -2,6 +2,8 @@
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import { getQuestions } from '../lib/questions';
const dispatch = createEventDispatcher();
export let survey = null;
@ -60,6 +62,23 @@
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="direct" class="col-form-label col-form-label-sm">Question en direct</label>
</div>
<div class="col-sm-8">
{#await getQuestions(survey.id) then questions}
<select id="direct" class="form-select form-select-sm" bind:value={survey.direct}>
<option value={null}>Pas de direct</option>
<option value={0}>Pause</option>
{#each questions as question (question.id)}
<option value={question.id}>{question.id} - {question.title}</option>
{/each}
</select>
{/await}
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>

View file

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

View file

@ -6,6 +6,17 @@
import SurveyBadge from '../components/SurveyBadge.svelte';
import { getSurveys } from '../lib/surveys';
import { getScore } from '../lib/users';
let req_surveys = getSurveys();
export let direct = null;
req_surveys.then((surveys) => {
for (const survey of surveys) {
if (survey.direct) {
direct = survey;
}
}
});
</script>
<table class="table table-striped table-hover mb-0">
@ -18,7 +29,7 @@
{/if}
</tr>
</thead>
{#await getSurveys()}
{#await req_surveys}
<tr>
<td colspan="5" class="text-center py-3">
<div class="spinner-border mx-3" role="status"></div>
@ -36,7 +47,7 @@
</th>
</tr>
{/if}
<tr on:click={e => goto($user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
<tr on:click={e => goto(survey.direct?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
<td>
{survey.title}
<SurveyBadge {survey} class="float-end" />

View file

@ -8,12 +8,13 @@ class Survey {
}
}
update({ id, title, promo, group, shown, corrected, start_availability, end_availability }) {
update({ id, title, promo, group, shown, direct, corrected, start_availability, end_availability }) {
this.id = id;
this.title = title;
this.promo = promo;
this.group = group;
this.shown = shown;
this.direct = direct;
this.corrected = corrected;
if (this.start_availability != start_availability) {
this.start_availability = start_availability;

View file

@ -2,6 +2,8 @@
import { user } from '../stores/user';
import SurveyList from '../components/SurveyList.svelte';
import ValidateSubmissions from '../components/ValidateSubmissions.svelte';
let direct = null;
</script>
<div class="card bg-light">
@ -20,6 +22,12 @@
</div>
{/if}
{#if direct}
<div class="alert alert-warning" role="alert">
<strong>Rejoins le cours maintenant&nbsp;!</strong> Il y a actuellement un questionnaire en direct&nbsp;: {direct.title}. <a href="surveys/{direct.id}/live">Clique ici pour le rejoindre</a>.
</div>
{/if}
<p class="lead">Tu as fait les rendus suivants&nbsp;:</p>
</div>
<div class="d-none d-md-block col-md-auto">
@ -38,6 +46,6 @@
Vous devez <a href="auth/CRI" target="_self">vous identifier</a> pour accéder au contenu.
</p>
{/if}
<SurveyList />
<SurveyList bind:direct={direct} />
</div>
</div>

View file

@ -0,0 +1,370 @@
<script context="module">
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
sid: params.sid,
},
};
}
</script>
<script>
import { user } from '../../../stores/user';
import SurveyAdmin from '../../../components/SurveyAdmin.svelte';
import SurveyBadge from '../../../components/SurveyBadge.svelte';
import { getQuestions } from '../../../lib/questions';
import { getUsers } from '../../../lib/users';
export let surveyP;
export let sid;
let survey;
let req_questions;
surveyP.then((s) => {
survey = s;
updateQuestions();
if (survey.direct !== null) {
wsconnect();
}
});
function updateQuestions() {
req_questions = getQuestions(survey.id);
}
let ws = null;
let ws_up = false;
let wsstats = null;
let current_question = null;
let responses = {};
let users = {};
function updateUsers() {
getUsers().then((usr) => {
const tmp = { };
for (const u of usr) {
tmp[u.id.toString()] = u;
}
users = tmp;
});
}
updateUsers();
let responsesbyid = { };
$: {
const tmp = { };
for (const response in responses) {
if (!tmp[response]) tmp[response] = [];
for (const r in responses[response]) {
tmp[response].push(responses[response][r]);
}
}
responsesbyid = tmp;
}
function wsconnect() {
if (ws !== null) return;
ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws-admin`);
ws.addEventListener("open", () => {
ws_up = true;
ws.send('{"action":"get_responses"}');
ws.send('{"action":"get_stats"}');
});
ws.addEventListener("close", (e) => {
ws_up = false;
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
ws = null;
setTimeout(function() {
wsconnect();
}, 1500);
});
ws.addEventListener("error", (err) => {
ws_up = false;
console.log('Socket closed due to error.', err.message);
ws = null;
});
ws.addEventListener("message", (message) => {
const data = JSON.parse(message.data);
console.log(data);
if (data.action && data.action == "new_question") {
current_question = data.question;
} else if (data.action && data.action == "stats") {
wsstats = data.stats;
} else if (data.action && data.action == "new_response") {
if (!responses[data.question]) responses[data.question] = {};
responses[data.question][data.user] = data.value;
} else {
current_question = null;
}
});
}
</script>
{#await surveyP then survey}
{#if $user && $user.is_admin}
{#if survey.direct !== null}
<a href="surveys/{survey.id}/live" class="btn btn-danger ms-1 float-end" title="Aller au direct"><i class="bi bi-film"></i></a>
<button
type="button"
class="btn btn-primary ms-1 float-end"
on:click={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
>
Terminer
</button>
{/if}
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses"><i class="bi bi-files"></i></a>
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
<small class="text-muted">
Administration
</small>
</h2>
{#if survey.direct !== null}
<div
class="badge rounded-pill ms-2"
class:bg-success={ws_up}
class:bg-danger={!ws_up}
>
{#if ws_up}Connecté{:else}Déconnecté{/if}
</div>
{:else}
<SurveyBadge
class="mx-2"
{survey}
/>
{/if}
</div>
{#if survey.direct === null}
<SurveyAdmin
{survey}
/>
{:else}
{#await req_questions}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des questions &hellip;</span>
</div>
{:then questions}
<div class="card my-3">
<table class="table table-hover table-striped mb-0">
<thead>
<tr>
<th>
Question
<button
type="button"
class="btn btn-sm btn-info ms-1"
on:click={updateQuestions}
title="Rafraîchir les questions"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</th>
<th>
Réponses
</th>
<th>
Actions
<button
type="button"
class="btn btn-sm btn-primary"
disabled={!current_question || !ws_up}
on:click={() => { ws.send('{"action":"pause"}')} }
>
Pause
</button>
</th>
</tr>
</thead>
<tbody>
{#each questions as question (question.id)}
<tr>
<td>
{#if responses[question.id]}
<a href="surveys/{sid}/admin#q{question.id}_res">
{question.title}
</a>
{:else}
{question.title}
{/if}
</td>
<td>
{#if responses[question.id]}
{Object.keys(responses[question.id]).length}
{:else}
0
{/if}
{#if wsstats}/ {wsstats.nb_clients}{/if}
</td>
<td>
<button
type="button"
class="btn btn-sm btn-primary"
disabled={question.id === current_question || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "question":' + question.id + '}')} }
>
Lancer cette question
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{/if}
<hr>
<button
type="button"
class="btn btn-sm btn-info ms-1 float-end"
on:click={() => { ws.send('{"action":"get_responses"}') }}
title="Rafraîchir les réponses"
>
<i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-card-checklist"></i>
</button>
<h3>
Réponses
</h3>
{#if Object.keys(responses).length}
{#each Object.keys(responses) as q, qid (qid)}
{#await req_questions then questions}
{#each questions as question}
{#if question.id == q}
<h4 id="q{question.id}_res">
{question.title}
</h4>
{#if question.kind == 'ucq'}
{#await question.getProposals()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des propositions &hellip;</span>
</div>
{:then proposals}
<div class="card mb-4">
<table class="table table-sm table-striped table-hover mb-0">
<tbody>
{#each proposals as proposal (proposal.id)}
<tr>
<td>
{proposal.label}
</td>
<td>
{responsesbyid[q].filter((e) => e == proposal.id.toString()).length}/{responsesbyid[q].length}
</td>
<td>
{Math.trunc(responsesbyid[q].filter((e) => e == proposal.id.toString()).length / responsesbyid[q].length * 1000)/10}&nbsp;%
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{:else if question.kind == 'mcq'}
{#await question.getProposals()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des propositions &hellip;</span>
</div>
{:then proposals}
<div class="card mb-4">
<table class="table table-sm table-striped table-hover mb-0">
<tbody>
{#each proposals as proposal (proposal.id)}
<tr>
<td>
{proposal.label}
</td>
<td>
{responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length}/{responsesbyid[q].length}
</td>
<td>
{Math.trunc(responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length / responsesbyid[q].length * 1000)/10}&nbsp;%
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{:else}
<div class="card mb-4">
<ul class="list-group list-group-flush">
{#each Object.keys(responses[q]) as user, rid (rid)}
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
<span>
{responses[q][user]}
</span>
<a href="users/{user}" target="_blank" class="badge bg-dark rounded-pill">
{#if users && users[user]}
{users[user].login}
{:else}
{user}
{/if}
</a>
</li>
{/each}
</ul>
</div>
{/if}
{/if}
{/each}
{/await}
{/each}
{/if}
<hr>
<button
type="button"
class="btn btn-sm btn-info ms-1 float-end"
on:click={() => { ws.send('{"action":"get_stats"}') }}
title="Rafraîchir les stats"
>
<i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-123"></i>
</button>
<button
type="button"
class="btn btn-sm btn-primary ms-1 float-end"
title="Rafraîchir la liste des utilisateurs"
on:click={updateUsers}
>
<i class="bi bi-arrow-clockwise"></i>
<i class="bi bi-people"></i>
</button>
<h3>
Connectés
{#if wsstats}
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
{/if}
</h3>
{#if wsstats}
<div class="row row-cols-5 py-3">
{#each wsstats.users as login, lid (lid)}
<div class="col">
<div class="card">
<img alt="{login}" src="//photos.cri.epita.fr/thumb/{login}" class="card-img-top">
<div class="card-footer text-center text-truncate p-0">
<a href="users/{login}" target="_blank">
{login}
</a>
</div>
</div>
</div>
{/each}
</div>
{/if}
{/await}

View file

@ -11,6 +11,8 @@
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '../../../stores/user';
import SurveyAdmin from '../../../components/SurveyAdmin.svelte';
import SurveyBadge from '../../../components/SurveyBadge.svelte';
@ -20,13 +22,26 @@
export let surveyP;
$: {
if (surveyP) {
surveyP.then((survey) => {
if (survey.direct && !$user.is_admin) {
goto(`surveys/${survey.id}/live`);
}
})
}
}
let edit = false;
</script>
{#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>
<button class="btn btn-primary ms-1 float-end" on:click={() => { edit = !edit; } } title="Éditer"><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"><i class="bi bi-files"></i></a>
{#if survey.direct}
<a href="surveys/{survey.id}/live" class="btn btn-danger ms-1 float-end" title="Aller au direct"><i class="bi bi-film"></i></a>
{/if}
{/if}
<div class="d-flex align-items-center">
<h2>

View file

@ -0,0 +1,118 @@
<script context="module">
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
sid: params.sid,
},
};
}
</script>
<script>
import { user } from '../../../stores/user';
import SurveyBadge from '../../../components/SurveyBadge.svelte';
import QuestionForm from '../../../components/QuestionForm.svelte';
import { getQuestion } from '../../../lib/questions';
export let surveyP;
export let sid;
let survey;
surveyP.then((s) => survey = s);
let ws_up = false;
let show_question = null;
let value;
function wsconnect() {
const ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`);
ws.addEventListener("open", () => {
ws_up = true;
});
ws.addEventListener("close", (e) => {
ws_up = false;
show_question = false;
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
setTimeout(function() {
wsconnect();
}, 1500);
});
ws.addEventListener("error", (err) => {
ws_up = false;
console.log('Socket closed due to error.', err.message);
});
ws.addEventListener("message", (message) => {
const data = JSON.parse(message.data);
console.log(data);
if (data.action && data.action == "new_question") {
show_question = data.question;
} else {
show_question = null;
}
});
}
wsconnect();
function sendValue() {
if (show_question && value) {
survey.submitAnswers([{"id_question": show_question, "value": value}], $user.id_user).then((response) => {
console.log("Vos réponses ont bien étés sauvegardées.");
}, (error) => {
value = null;
console.log("Une erreur s'est produite durant l'envoi de vos réponses : " + error + "<br>Veuillez réessayer dans quelques instants.");
});
}
}
</script>
{#await surveyP then survey}
{#if $user && $user.is_admin}
<a href="surveys/{survey.id}/admin" class="btn btn-primary ms-1 float-end" title="Aller à l'interface d'administration"><i class="bi bi-pencil"></i></a>
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses"><i class="bi bi-files"></i></a>
{/if}
<div class="d-flex align-items-center mb-5">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
</h2>
<div
class="badge rounded-pill ms-2"
class:bg-success={ws_up}
class:bg-danger={!ws_up}
>
{#if ws_up}Connecté{:else}Déconnecté{/if}
</div>
</div>
<form on:submit|preventDefault={sendValue}>
{#if show_question}
{#await getQuestion(show_question)}
Please wait
{:then question}
<QuestionForm
qid={show_question}
{question}
bind:value={value}
on:change={sendValue}
>
<!--div class="progress" style="border-radius: 0; height: 4px">
<div class="progress-bar" role="progressbar" style="width: 25%"></div>
</div-->
</QuestionForm>
{/await}
{:else if ws_up}
<h2 class="text-center">
Pas de question actuellement
</h2>
{:else}
<h2 class="text-center">
La session est terminée. <small class="text-muted">On se retrouve une prochaine fois&hellip;</small>
</h2>
{/if}
</form>
{/await}