Move new ui to ui directory
This commit is contained in:
parent
bb72c351ac
commit
f85758ef33
56 changed files with 0 additions and 0 deletions
146
ui/src/routes/__layout.svelte
Normal file
146
ui/src/routes/__layout.svelte
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<script context="module">
|
||||
import { user } from '../stores/user';
|
||||
let stop_refresh = false;
|
||||
|
||||
let refresh_interval_auth = null;
|
||||
async function refresh_auth(cb=null, interval=null) {
|
||||
if (refresh_interval_auth)
|
||||
clearInterval(refresh_interval_auth);
|
||||
if (interval === null) {
|
||||
interval = Math.floor(Math.random() * 200000) + 200000;
|
||||
}
|
||||
if (stop_refresh) {
|
||||
return;
|
||||
}
|
||||
refresh_interval_auth = setInterval(refresh_auth, interval);
|
||||
|
||||
const res = await fetch('api/auth', {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
const auth = await res.json();
|
||||
user.set(auth);
|
||||
} else {
|
||||
user.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
export async function load({ props, stuff, url }) {
|
||||
refresh_auth();
|
||||
|
||||
const rroutes = url.pathname.split('/');
|
||||
|
||||
return {
|
||||
props: {
|
||||
...props,
|
||||
rroute: rroutes.length>1?rroutes[1]:'',
|
||||
},
|
||||
stuff: {
|
||||
...stuff,
|
||||
refresh_auth,
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Toaster from '../components/Toaster.svelte';
|
||||
|
||||
export let rroute = '';
|
||||
|
||||
function switchAdminMode() {
|
||||
var tmp = $user.is_admin;
|
||||
$user.is_admin = $user.was_admin || false;
|
||||
$user.was_admin = tmp;
|
||||
}
|
||||
|
||||
function disconnectCurrentUser() {
|
||||
fetch('api/auth/logout', {
|
||||
method: 'POST'
|
||||
}).then((response) => {
|
||||
refresh_auth();
|
||||
});
|
||||
}
|
||||
</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=".">
|
||||
SRS
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="loggedMenu">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="adlin" target="_self">AdLin</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="fic" target="_self">FIC</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" class:active={rroute === 'surveys'} href="surveys">
|
||||
Questionnaires
|
||||
</a>
|
||||
</li>
|
||||
{#if $user && $user.is_admin}
|
||||
<li class="nav-item"><a class="nav-link" class:active={rroute === 'users'} href="users">Étudiants</a></li>
|
||||
{/if}
|
||||
<li class="nav-item"><a class="nav-link" href="virli" target="_self">VIRLI</a></li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{#if $user}
|
||||
{#if $user.was_admin}
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-dark" on:click={switchAdminMode}>
|
||||
Vue admin
|
||||
</button>
|
||||
</li>
|
||||
{:else if $user.is_admin}
|
||||
<li class="nav-item">
|
||||
<button class="btn btn-light" on:click={switchAdminMode}>
|
||||
Vue étudiant
|
||||
</button>
|
||||
</li>
|
||||
{/if}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
|
||||
<img class="rounded-circle" src="//photos.cri.epita.fr/square/{$user.login}" alt="Menu" style="margin: -0.75em 0; max-height: 2.5em">
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" class:active={rroute === 'help'} href="help">Besoin d'aide ?</a></li>
|
||||
<li><a class="dropdown-item" class:active={rroute === 'bug-bounty'} href="bug-bounty">Bug Bounty</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<button class="dropdown-item" on:click={disconnectCurrentUser}>
|
||||
Se déconnecter
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{:else if $user === undefined}
|
||||
<li class="nav-item">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="nav-item">
|
||||
<a href="auth/CRI" target="_self" class="btn btn-dark">
|
||||
Se connecter
|
||||
</a>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-3">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<Toaster />
|
||||
71
ui/src/routes/auth.svelte
Normal file
71
ui/src/routes/auth.svelte
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<script context="module">
|
||||
import { session } from '$app/stores';
|
||||
|
||||
export function load() {
|
||||
if (session.id) {
|
||||
return { redirect: '/', status: 302 };
|
||||
}
|
||||
|
||||
return { };
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let auth = { username: "", password: "" };
|
||||
let pleaseWait = false;
|
||||
|
||||
function logmein() {
|
||||
pleaseWait = true;
|
||||
fetch('api/auth', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(auth),
|
||||
})
|
||||
.then((response) => {
|
||||
response.json().then((auth) => {
|
||||
pleaseWait = false;
|
||||
$session = auth;
|
||||
goto(".");
|
||||
})
|
||||
})
|
||||
.catch((response) => {
|
||||
pleaseWait = false;
|
||||
if (response.data)
|
||||
addToast({
|
||||
variant: "danger",
|
||||
title: "Connexion impossible",
|
||||
msg: (response.data ? response.data.errmsg : "Impossible de contacter le serveur"),
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<form class="col" on:submit|preventDefault={logmein}>
|
||||
<h2>Accès à votre compte</h2>
|
||||
<div class="mb-3">
|
||||
<label for="login" class="form-label">CRI login</label>
|
||||
<input class="form-control" id="login" bind:value={auth.username} placeholder="Entrer votre login" autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Mot de passe</label>
|
||||
<input type="password" class="form-control" id="password" bind:value={auth.password} placeholder="Mot de passe">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info">
|
||||
{#if pleaseWait}
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
{/if}
|
||||
Me connecter
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="col">
|
||||
<h2>OpenId Connect</h2>
|
||||
<div class="text-center">
|
||||
<a href="auth/CRI" class="btn btn-primary" target="_self">
|
||||
Me connecter avec mon compte CRI
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
89
ui/src/routes/bug-bounty.svelte
Normal file
89
ui/src/routes/bug-bounty.svelte
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<h2>Bug Bounty</h2>
|
||||
|
||||
<p class="lead">
|
||||
Comme tous les services accessibles en ligne, ce site présente un certain nombre de bugs et de vulnérabilités qui ne font pas partie des fonctionnalités attendues.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Par exception aux règles qui vous ont été données, vous êtes autorisés à rechercher des vulnérabilités sur tous les services (et leurs infrastructures afférentes) qui vous sont mis à disposition sur <code>nemunai.re</code>, selon les conditions suivantes :
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>vous ne devez pas <ins>entraver volontairement</ins> la progression de vos camarades ou le fonctionnement d'une partie de l'infrastructure ;</strong></li>
|
||||
<li><strong>vous devez maîtriser les outils que vous utiliser :</strong> certains outils mal maîtrisés peuvent bombarder de requêtes un service au point de le faire tomber. Les services mis à votre disposition ne constituent pas une plateforme d'entraînement à l'utilisation de ces outils, vous avez des machines virtuelles pour cela ;</li>
|
||||
<li><strong>vous devez rapporter rapidement à <a href="mailto:bounty@nemunai.re">bounty@nemunai.re</a> tous les bugs ou vulnérabilités que vous découvrez.</strong></li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
En plus du maintien des bénéfices que vous avez éventuellement pu obtenir (par exemple changer une note), vous obtiendrez également un bonus sur votre note finale.
|
||||
À titre indicatif, voici ce à quoi vous pouvez vous attendre :
|
||||
</p>
|
||||
|
||||
<div class="card mb-2">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Exemples de vulnérabilités</th>
|
||||
<th>Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Faute d'orthographe/grammaire, non conformité, bug, ...</td>
|
||||
<td>crédité dans le commit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>XSS, CSRF, injection SQL/LDAP, ...</td>
|
||||
<td>1 point</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Violation de permission, fuite d'informations, ...</td>
|
||||
<td>2 points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exécution arbitraire de code</td>
|
||||
<td>3 points</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Exécution arbitraire de code sur l'hôte</td>
|
||||
<td>5 points</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Lorsque vous découvrez une vulnérabilité en groupe, précisez les noms et le rôle que chacun a eu dans la découverte.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-warning d-flex">
|
||||
<i class="bi bi-exclamation-triangle me-3"></i>
|
||||
<span>
|
||||
À toute fin utile, l'usage et la non-divulgation d'une vulnérabilité sont <a href="https://www.legifrance.gouv.fr/codes/id/LEGISCTA000006149839/" target="_blank">sanctionnables</a>.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5 mb-3">Hall of Fame</h3>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
Il était toujours possible de répondre aux questionnaires après l'heure de clôture.
|
||||
<span class="badge bg-success shadow-lg">+2 pts</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-6">
|
||||
<img class="img-thumbnail" src="//photos.cri.epita.fr/mahe.charpy" alt="mahe.charpy">
|
||||
<img class="img-thumbnail" src="//photos.cri.epita.fr/albin.parou" alt="albin.parou">
|
||||
<img class="img-thumbnail" src="//photos.cri.epita.fr/sebastien.januszczak" alt="sebastien.januszczak">
|
||||
<img class="img-thumbnail" src="//photos.cri.epita.fr/clement.lanata" alt="clement.lanata">
|
||||
<img class="img-thumbnail" src="//photos.cri.epita.fr/alexandre.delorme" alt="alexandre.delorme">
|
||||
<img class="img-thumbnail" src="//photos.cri.epita.fr/justin.puchelle" alt="justin.puchelle">
|
||||
</div>
|
||||
<p class="card-text mt-3">
|
||||
Divulguée et corrigée le 19 novembre 2021.
|
||||
<a href="https://git.nemunai.re/srs/atsebay.t/commit/5c53d2eaea9e7233bc8a08de2f40c040c0700c3e" target="_blank">Commit</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5"></div>
|
||||
17
ui/src/routes/grades/[promo].svelte
Normal file
17
ui/src/routes/grades/[promo].svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script context="module">
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
props: {
|
||||
promo: params.promo,
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import StudentGrades from '../../components/StudentGrades.svelte';
|
||||
|
||||
export let promo;
|
||||
</script>
|
||||
|
||||
<StudentGrades {promo} />
|
||||
5
ui/src/routes/grades/index.svelte
Normal file
5
ui/src/routes/grades/index.svelte
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import StudentGrades from '../../components/StudentGrades.svelte';
|
||||
</script>
|
||||
|
||||
<StudentGrades />
|
||||
40
ui/src/routes/help.svelte
Normal file
40
ui/src/routes/help.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
function needhelp() {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<h2>Besoin d'aide ?</h2>
|
||||
|
||||
<p class="lead">
|
||||
Vous êtes nombreux et l'on n'est malheureusement pas en mesure de vous suivre régulièrement individuellement.
|
||||
Nous restons néanmoins toujours disponibles lorsque vous avez besoin de notre aide.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
D'une manière générale, si vous avez des problèmes, n'hésitez pas à contacter le professeur, que ce soit en cours ou par mail.
|
||||
Il vaut mieux mettre des mots soi-même sur un problème que l'on rencontre plutôt que d'attendre le dernier moment, en se disant qu'on aura le temps de trouver une solution.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Peut-être que tu as raté·e plusieurs rendus, ou peut-être que tu ne te sens plus le courage de continuer les projets.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Si tu souhaites me parler d'une situation qui t'a troublé·e, d'un problème que tu rencontres ou me faire une remarque,
|
||||
n'hésite pas à venir me voir lors d'un cours, par exemple à la pause ou à la fin ;
|
||||
je suis aussi joignable <a href="mailto:nemunaire@nemunai.re">par e-mail</a> ou bien <a href="https://matrix.to/#/@nemunaire:nemunai.re">sur Matrix</a> ou Teams.
|
||||
</p>
|
||||
|
||||
<p class="mt-4">
|
||||
Si tu souhaites juste avoir un peu plus d'attention, soit parce que tu te sens à l'écart, en difficulté ou autre :
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
on:click={needhelp}
|
||||
>
|
||||
Clique ce bouton
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<div class="mb-5"></div>
|
||||
51
ui/src/routes/index.svelte
Normal file
51
ui/src/routes/index.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
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">
|
||||
<div class="card-body">
|
||||
{#if $user}
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h1 class="card-text">
|
||||
Bienvenue {$user.firstname} !
|
||||
</h1>
|
||||
<hr class="my-4">
|
||||
|
||||
{#if $user.promo != $user.current_promo}
|
||||
<div class="alert alert-primary" role="alert">
|
||||
<strong>Es-tu un {$user.current_promo} ?</strong> Tu es actuellement enregistré comme un {$user.promo}, ce qui ne te permet pas d'accéder aux questionnaires de la promo {$user.current_promo}. <a href="mailto:nemunaire@nemunai.re?subject=Mauvaise promotion sur srs.nemunai.re&body=Bonjour, Je ne suis pas enregistré dans la bonne promotion sur le site srs.nemunai.re. Cordialement,">Contacte-moi</a> pour corriger cela.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if direct}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>Rejoins le cours maintenant !</strong> Il y a actuellement un questionnaire en direct : {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 :</p>
|
||||
</div>
|
||||
<div class="d-none d-md-block col-md-auto">
|
||||
<img class="img-thumbnail" src="https://photos.cri.epita.fr/thumb/{$user.login}" alt="avatar {$user.login}" style="max-height: 150px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ValidateSubmissions />
|
||||
|
||||
<p class="lead">Voici la liste des questionnaires :</p>
|
||||
{:else}
|
||||
<p class="card-text lead">
|
||||
Vous voici arrivés sur le site dédié aux cours d'<a href="https://adlin.nemunai.re/">Administration Linux avancée</a>, du <a href="https://srs.nemunai.re/fic/">FIC</a> et de <a href="https://virli.nemunai.re/">Virtualisation légère</a>.
|
||||
</p>
|
||||
<p class="card-text">
|
||||
Vous devez <a href="auth/CRI" target="_self">vous identifier</a> pour accéder au contenu.
|
||||
</p>
|
||||
{/if}
|
||||
<SurveyList bind:direct={direct} />
|
||||
</div>
|
||||
</div>
|
||||
39
ui/src/routes/surveys/[sid]/__layout.svelte
Normal file
39
ui/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}
|
||||
370
ui/src/routes/surveys/[sid]/admin.svelte
Normal file
370
ui/src/routes/surveys/[sid]/admin.svelte
Normal 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"><</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 …</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"}')} }
|
||||
>
|
||||
<i class="bi bi-pause-fill"></i>
|
||||
</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 + '}')} }
|
||||
>
|
||||
<i class="bi bi-play-fill"></i>
|
||||
</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 …</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} %
|
||||
</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 …</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} %
|
||||
</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}
|
||||
66
ui/src/routes/surveys/[sid]/index.svelte
Normal file
66
ui/src/routes/surveys/[sid]/index.svelte
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<script context="module">
|
||||
import { getSurvey } from '../../../lib/surveys';
|
||||
|
||||
export async function load({ params, stuff }) {
|
||||
return {
|
||||
props: {
|
||||
surveyP: stuff.survey,
|
||||
},
|
||||
};
|
||||
}
|
||||
</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';
|
||||
import SurveyQuestions from '../../../components/SurveyQuestions.svelte';
|
||||
import { getSurvey } from '../../../lib/surveys';
|
||||
import { getQuestions } from '../../../lib/questions';
|
||||
|
||||
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"><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>
|
||||
<a href="surveys/" class="text-muted" style="text-decoration: none"><</a>
|
||||
{survey.title}
|
||||
</h2>
|
||||
<SurveyBadge class="ms-2" {survey} />
|
||||
</div>
|
||||
|
||||
{#if $user.is_admin && edit}
|
||||
<SurveyAdmin {survey} on:saved={() => edit = false} />
|
||||
{/if}
|
||||
|
||||
{#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}
|
||||
<SurveyQuestions {survey} {questions} />
|
||||
{/await}
|
||||
{/await}
|
||||
151
ui/src/routes/surveys/[sid]/live.svelte
Normal file
151
ui/src/routes/surveys/[sid]/live.svelte
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
<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 { ToastsStore } from '../../../stores/toasts';
|
||||
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;
|
||||
|
||||
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() {
|
||||
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 && !nosend) {
|
||||
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;
|
||||
ToastsStore.addErrorToast({
|
||||
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez 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"><</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 req_question}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement d'une nouvelle question …</span>
|
||||
</div>
|
||||
{: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>
|
||||
{#if question.kind != 'mcq' && question.kind != 'ucq'}
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Soumettre la réponse
|
||||
</button>
|
||||
{/if}
|
||||
{/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…</small>
|
||||
</h2>
|
||||
{/if}
|
||||
</form>
|
||||
{/await}
|
||||
156
ui/src/routes/surveys/[sid]/responses/[rid].svelte
Normal file
156
ui/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
ui/src/routes/surveys/[sid]/responses/index.svelte
Normal file
85
ui/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}
|
||||
8
ui/src/routes/surveys/index.svelte
Normal file
8
ui/src/routes/surveys/index.svelte
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { user } from '../../stores/user';
|
||||
import SurveyList from '../../components/SurveyList.svelte';
|
||||
</script>
|
||||
|
||||
<div class="card bg-light">
|
||||
<SurveyList />
|
||||
</div>
|
||||
111
ui/src/routes/users/[uid]/index.svelte
Normal file
111
ui/src/routes/users/[uid]/index.svelte
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<script context="module">
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
props: {
|
||||
uid: params.uid,
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import UserSurveys from '../../../components/UserSurveys.svelte';
|
||||
import { user } from '../../../stores/user';
|
||||
import { getSurveys } from '../../../lib/surveys';
|
||||
import { getUser, getUserGrade, getUserScore } from '../../../lib/users';
|
||||
|
||||
export let uid;
|
||||
|
||||
let allPromos = false;
|
||||
</script>
|
||||
|
||||
{#await getUser(uid)}
|
||||
<h2>
|
||||
Étudiant
|
||||
</h2>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border me-2" role="status"></div>
|
||||
Chargement des détails…
|
||||
</div>
|
||||
{:then student}
|
||||
<div class="card mb-5">
|
||||
<div class="card-header d-flex justify-content-center align-items-center">
|
||||
<h2 class="card-title text-center">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
{#if student.firstname}
|
||||
{student.firstname} {student.lastname}
|
||||
{:else}
|
||||
{student.login}
|
||||
{/if}
|
||||
</h2>
|
||||
{#if student.promo}
|
||||
<span class="badge bg-success ms-1">{student.promo}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3 text-center">
|
||||
<a href="https://photos.cri.epita.fr/{student.login}" target="_blank">
|
||||
<img src="https://photos.cri.epita.fr/thumb/{student.login}" alt="avatar" class="img-thumbnail" style="max-height: 250px">
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<dl class="row">
|
||||
<dt class="col-2">ID</dt>
|
||||
<dd class="col-10">{student.id}</dd>
|
||||
|
||||
<dt class="col-2">Login</dt>
|
||||
<dd class="col-10">
|
||||
<a href="//cri.epita.fr/users/{student.login}" target="_blank">
|
||||
{student.login}
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-2">E-mail</dt>
|
||||
<dd class="col-10"><a href="mailto:{student.email}">{student.email}</a></dd>
|
||||
|
||||
<dt class="col-2">Nom</dt>
|
||||
<dd class="col-10">{student.lastname}</dd>
|
||||
|
||||
<dt class="col-2">Prénom</dt>
|
||||
<dd class="col-10">{student.firstname}</dd>
|
||||
|
||||
<dt class="col-2">Inscription</dt>
|
||||
<dd class="col-10">{student.time}</dd>
|
||||
|
||||
<dt class="col-2">Groupes</dt>
|
||||
<dd class="col-10">
|
||||
<ul ng-if="student.groups">
|
||||
{#each student.groups.split(',').slice(1) as g, gid (gid)}
|
||||
<li>
|
||||
<a href="https://cri.epita.fr/group/{g}/">{g}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</dd>
|
||||
|
||||
<dt class="col-2">Admin</dt>
|
||||
<dd class="col-10">{student.is_admin?"Oui":"Non"}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<button
|
||||
class="btn btn-secondary float-end"
|
||||
class:active={allPromos}
|
||||
on:click={e => allPromos = !allPromos}
|
||||
>
|
||||
Toutes les promos
|
||||
</button>
|
||||
<h3 class="card-title">
|
||||
Questionnaires
|
||||
</h3>
|
||||
</div>
|
||||
<UserSurveys
|
||||
{student}
|
||||
{allPromos}
|
||||
/>
|
||||
</div>
|
||||
{/await}
|
||||
64
ui/src/routes/users/[uid]/surveys/[sid].svelte
Normal file
64
ui/src/routes/users/[uid]/surveys/[sid].svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script context="module">
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
props: {
|
||||
sid: params.sid,
|
||||
uid: params.uid,
|
||||
}
|
||||
};
|
||||
}
|
||||
</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';
|
||||
import { getUser } from '../../../../lib/users';
|
||||
|
||||
export let sid;
|
||||
export let uid;
|
||||
</script>
|
||||
|
||||
{#await getUser(uid)}
|
||||
<h2>
|
||||
Étudiant
|
||||
</h2>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border me-2" role="status"></div>
|
||||
Chargement des détails…
|
||||
</div>
|
||||
{:then student}
|
||||
{#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}
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="users/{student.id}/surveys/" class="text-muted" style="text-decoration: none">{student.login}</a> /
|
||||
{survey.title}
|
||||
</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}
|
||||
<SurveyQuestions {survey} {questions} id_user={uid} />
|
||||
{/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}
|
||||
{/await}
|
||||
64
ui/src/routes/users/[uid]/surveys/index.svelte
Normal file
64
ui/src/routes/users/[uid]/surveys/index.svelte
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script context="module">
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
props: {
|
||||
uid: params.uid,
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import UserSurveys from '../../../../components/UserSurveys.svelte';
|
||||
import { user } from '../../../../stores/user';
|
||||
import { getSurveys } from '../../../../lib/surveys';
|
||||
import { getUser, getUserGrade, getUserScore } from '../../../../lib/users';
|
||||
|
||||
export let uid;
|
||||
|
||||
let allPromos = false;
|
||||
</script>
|
||||
|
||||
{#await getUser(uid)}
|
||||
<h2>
|
||||
Étudiant
|
||||
</h2>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border me-2" role="status"></div>
|
||||
Chargement des détails…
|
||||
</div>
|
||||
{:then student}
|
||||
<div class="card mb-5">
|
||||
<div class="card-header d-flex justify-content-center align-items-center">
|
||||
<h2 class="card-title text-center">
|
||||
<a href="users/{student.id}"><i class="bi bi-chevron-left"></i></a>
|
||||
<i class="bi bi-person-fill"></i>
|
||||
{#if student.firstname}
|
||||
{student.firstname} {student.lastname}
|
||||
{:else}
|
||||
{student.login}
|
||||
{/if}
|
||||
</h2>
|
||||
{#if student.promo}
|
||||
<span class="badge bg-success ms-1">{student.promo}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card-header">
|
||||
<button
|
||||
class="btn btn-secondary float-end"
|
||||
class:active={allPromos}
|
||||
on:click={e => allPromos = !allPromos}
|
||||
>
|
||||
Toutes les promos
|
||||
</button>
|
||||
<h3 class="card-title">
|
||||
Questionnaires
|
||||
</h3>
|
||||
</div>
|
||||
<UserSurveys
|
||||
{student}
|
||||
{allPromos}
|
||||
/>
|
||||
</div>
|
||||
{/await}
|
||||
79
ui/src/routes/users/index.svelte
Normal file
79
ui/src/routes/users/index.svelte
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { user } from '../../stores/user';
|
||||
import DateFormat from '../../components/DateFormat.svelte';
|
||||
import { getUsers, getPromos } from '../../lib/users';
|
||||
|
||||
function showUser(user) {
|
||||
goto(`users/${user.id}`)
|
||||
}
|
||||
|
||||
let filterPromo = "";
|
||||
</script>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<a href="grades" class="btn btn-success ml-1 float-end" title="Notes">
|
||||
<i class="bi bi-files"></i>
|
||||
</a>
|
||||
{#await getPromos() then promos}
|
||||
<div class="float-end me-2">
|
||||
<select class="form-select" bind:value={filterPromo}>
|
||||
<option value="">-</option>
|
||||
{#each promos as promo, pid (pid)}
|
||||
<option value={promo}>{promo}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
<h2>
|
||||
Étudiants
|
||||
</h2>
|
||||
|
||||
{#await getUsers()}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-danger mx-3" role="status"></div>
|
||||
<span>Chargement des étudiants …</span>
|
||||
</div>
|
||||
{:then users}
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Login</th>
|
||||
<th>E-mail</th>
|
||||
<th>Nom</th>
|
||||
<th>Prénom</th>
|
||||
<th>Date d'inscription</th>
|
||||
<th>Promo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users.filter((u) => (filterPromo === "" || filterPromo === u.promo)) as u, uid (u.id)}
|
||||
<tr
|
||||
class:bg-danger={u.is_admin}
|
||||
>
|
||||
<td>
|
||||
{u.id}
|
||||
<div class="d-inline-block float-end" style="max-height: 1.5em">
|
||||
<img src="//photos.cri.epita.fr/square/{u.login}" alt={u.login} style="max-height: 2.5em; margin-top: -0.33em">
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="users/{u.login}">{u.login}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="mailto:{u.email}">{u.email}</a>
|
||||
</td>
|
||||
<td>{u.lastname}</td>
|
||||
<td>{u.firstname}</td>
|
||||
<td>
|
||||
<DateFormat date={u.time} dateStyle="short" timeStyle="medium" />
|
||||
</td>
|
||||
<td>{u.promo}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/await}
|
||||
Reference in a new issue