svelte-migrate: updated files

This commit is contained in:
nemunaire 2022-12-12 10:24:30 +01:00
parent 4d6149760d
commit ff5a2eef65
36 changed files with 1286 additions and 1418 deletions

View File

@ -10,7 +10,7 @@ import (
"net/http"
)
//go:embed ui/build/* ui/build/css/* ui/build/surveys/* ui/build/_app/* ui/build/_app/immutable/* ui/build/_app/immutable/pages/* ui/build/_app/immutable/pages/surveys/* ui/build/_app/immutable/pages/surveys/_sid_/* ui/build/_app/immutable/pages/surveys/_sid_/responses/* ui/build/_app/immutable/pages/grades/* ui/build/_app/immutable/pages/works/* ui/build/_app/immutable/pages/works/_wid_/* ui/build/_app/immutable/pages/users/* ui/build/_app/immutable/pages/users/_uid_/* ui/build/_app/immutable/pages/users/_uid_/surveys/* ui/build/_app/immutable/chunks/* ui/build/_app/immutable/assets/* ui/build/img/* ui/build/works/*
//go:embed all:ui/build
var _assets embed.FS
var Assets http.FileSystem

View File

@ -48,7 +48,6 @@ func serveOrReverse(forced_url string) func(c *gin.Context) {
}
func declareStaticRoutes(router *gin.Engine) {
router.GET("/@fs/*_", serveOrReverse(""))
router.GET("/", serveOrReverse(""))
router.GET("/_app/*_", serveOrReverse(""))
router.GET("/auth/", serveOrReverse("/"))
@ -74,7 +73,7 @@ func declareStaticRoutes(router *gin.Engine) {
router.GET("/.svelte-kit/*_", serveOrReverse(""))
router.GET("/node_modules/*_", serveOrReverse(""))
router.GET("/@vite/*_", serveOrReverse(""))
router.GET("/__vite_ping", serveOrReverse(""))
router.GET("/@fs/*_", serveOrReverse(""))
router.GET("/src/*_", serveOrReverse(""))
}
}

View File

@ -1,6 +0,0 @@
export async function handle({ event, resolve }) {
const response = await resolve(event, {
ssr: false,
});
return response;
}

View File

@ -8,14 +8,10 @@ function createUserStore() {
set: (auth) => {
update((m) => auth);
},
update: (res_auth, cb=null) => {
update: (res_auth) => {
if (res_auth.status === 200) {
res_auth.json().then((auth) => {
update((m) => (Object.assign(m?m:{}, auth)));
if (cb) {
cb(my);
}
});
} else if (res_auth.status >= 400 && res_auth.status < 500) {
update((m) => (null));
@ -24,4 +20,14 @@ function createUserStore() {
};
}
export async function refresh_auth() {
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 const user = createUserStore();

16
ui/src/routes/+layout.js Normal file
View File

@ -0,0 +1,16 @@
import { refresh_auth, user } from '$lib/stores/user';
export const ssr = false;
let refresh_interval_auth = null;
export async function load({ url }) {
refresh_interval_auth = setInterval(refresh_auth, Math.floor(Math.random() * 200000) + 200000);
refresh_auth();
const rroutes = url.pathname.split('/');
return {
rroute: rroutes.length>1?rroutes[1]:'',
};
}

View File

@ -1,51 +1,9 @@
<script context="module">
import { user } from '$lib/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 AuthButton from '$lib/components/AuthButton.svelte';
import Toaster from '$lib/components/Toaster.svelte';
import { refresh_auth, user } from '$lib/stores/user';
export let rroute = '';
export let data;
function switchAdminMode() {
var tmp = $user.is_admin;
@ -94,17 +52,17 @@
</li>
{/if}
<li class="nav-item">
<a class="nav-link" class:active={rroute === 'surveys'} href="surveys">
<a class="nav-link" class:active={data.rroute === 'surveys'} href="surveys">
Questionnaires
</a>
</li>
<li class="nav-item">
<a class="nav-link" class:active={rroute === 'works'} href="works">
<a class="nav-link" class:active={data.rroute === 'works'} href="works">
Travaux
</a>
</li>
{#if $user && $user.is_admin}
<li class="nav-item"><a class="nav-link" class:active={rroute === 'users'} href="users">Étudiants</a></li>
<li class="nav-item"><a class="nav-link" class:active={data.rroute === 'users'} href="users">Étudiants</a></li>
{/if}
<li class="nav-item"><a class="nav-link" href="virli" target="_self">VIRLI</a></li>
</ul>
@ -129,9 +87,9 @@
<img class="rounded-circle" src="//photos.cri.epita.fr/square/{$user.login}" alt="Menu" style="margin: -0.75em 0; max-height: 2.5em; border: 2px solid white;">
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" class:active={rroute === 'keys'} href="keys">Clef PGP</a></li>
<li><a class="dropdown-item" class:active={rroute === 'help'} href="help">Besoin d'aide&nbsp;?</a></li>
<li><a class="dropdown-item" class:active={rroute === 'bug-bounty'} href="bug-bounty">Bug Bounty</a></li>
<li><a class="dropdown-item" class:active={data.rroute === 'keys'} href="keys">Clef PGP</a></li>
<li><a class="dropdown-item" class:active={data.rroute === 'help'} href="help">Besoin d'aide&nbsp;?</a></li>
<li><a class="dropdown-item" class:active={data.rroute === 'bug-bounty'} href="bug-bounty">Bug Bounty</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item" on:click={disconnectCurrentUser}>

View File

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
cid: params.cid,
};
}

View File

@ -1,15 +1,3 @@
<script context="module">
import { getWork } from '$lib/works';
export async function load({ params }) {
return {
props: {
cid: params.cid,
},
};
}
</script>
<script>
import { goto } from '$app/navigation';
@ -17,12 +5,10 @@
import CategoryAdmin from '$lib/components/CategoryAdmin.svelte';
import { Category, getCategory } from '$lib/categories';
export let cid;
export let data;
let categoryP = null;
$: {
categoryP = getCategory(cid);
}
$: categoryP = getCategory(data.cid);
</script>
{#await categoryP then category}

View File

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
promo: params.promo,
};
}

View File

@ -1,17 +1,7 @@
<script context="module">
export async function load({ params }) {
return {
props: {
promo: params.promo,
}
};
}
</script>
<script>
import StudentGrades from '$lib/components/StudentGrades.svelte';
export let promo;
export let data;
</script>
<StudentGrades {promo} />
<StudentGrades promo={data.promo} />

View File

@ -0,0 +1,7 @@
export async function load({ url }) {
return {
secret: url.searchParams.get("secret"),
idsurvey: url.searchParams.get("survey"),
exportview_list: url.searchParams.get("graph_list")?false:true,
};
}

View File

@ -1,26 +1,13 @@
<script context="module">
export async function load({ url }) {
return {
props: {
secret: url.searchParams.get("secret"),
idsurvey: url.searchParams.get("survey"),
exportview_list: url.searchParams.get("graph_list")?false:true,
}
};
}
</script>
<script>
import { getSharedQuestions } from '$lib/questions';
import { getSharedSurvey } from '$lib/surveys';
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
export let secret;
export let idsurvey;
export let data;
let surveyP = getSharedSurvey(idsurvey, secret);
export let exportview_list = true;
let surveyP = null;
$: surveyP = getSharedSurvey(data.idsurvey, data.secret);
</script>
{#await surveyP then survey}
@ -32,7 +19,7 @@
<SurveyBadge class="ms-2" {survey} />
</div>
{#await getSharedQuestions(survey.id, secret)}
{#await getSharedQuestions(survey.id, data.secret)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des questions &hellip;</span>
@ -40,8 +27,8 @@
{:then questions}
{#each questions as question (question.id)}
<h3>{question.title}</h3>
{#if question.kind == "text" || (exportview_list && question.kind.indexOf("list") == 0)}
{#await question.getResponses(secret) then responses}
{#if question.kind == "text" || (data.exportview_list && question.kind.indexOf("list") == 0)}
{#await question.getResponses(data.secret) then responses}
{#each responses as response (response.id)}
<div class="card mb-2">
<div class="card-body">
@ -53,7 +40,7 @@
{/each}
{/await}
{:else}
<CorrectionPieChart {question} {secret} />
<CorrectionPieChart {question} secret={data.secret} />
{/if}
<hr class="mb-3">
{/each}

View File

@ -0,0 +1,9 @@
import { getSurvey } from '$lib/surveys';
export async function load({ params }) {
const survey = getSurvey(params.sid);
return {
survey,
};
}

View File

@ -1,27 +1,8 @@
<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;
export let data;
</script>
{#await survey}
{#await data.survey}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement du questionnaire &hellip;</span>

View File

@ -0,0 +1,5 @@
export async function load({ parent }) {
const stuff = await parent();
return stuff;
}

View File

@ -1,15 +1,3 @@
<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';
@ -19,62 +7,58 @@
import SurveyQuestions from '$lib/components/SurveyQuestions.svelte';
import { getQuestions } from '$lib/questions';
export let surveyP;
export let data;
let survey = null;
$: {
if (surveyP) {
surveyP.then((survey) => {
if (survey.direct && !$user.is_admin) {
goto(`surveys/${survey.id}/live`);
}
})
survey = data.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 $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}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
</h2>
<SurveyBadge class="ms-2" {survey} />
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
{#if $user && $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 &hellip;</span>
</div>
{#if $user && $user.is_admin && edit}
<SurveyAdmin {survey} on:saved={() => edit = false} />
{:then questions}
<SurveyQuestions {survey} {questions} />
{:catch error}
<div class="row mt-5">
<div class="d-none d-sm-block col-sm">
<hr>
</div>
<h3 class="col-sm-auto text-center text-muted mb-3"><label for="askquestion">Ce questionnaire n'est pas accessible</label></h3>
<div class="d-none d-sm-block col-sm">
<hr>
</div>
</div>
{#if survey.direct != null}
<div class="alert alert-warning">
<strong><a href="surveys/{survey.id}/live">Cliquez ici pour accéder au direct</a>.</strong> Il s'agit d'un questionnaire en direct, le questionnaire n'est pas accessible sur cette page.
</div>
{/if}
{#await getQuestions(survey.id)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des questions &hellip;</span>
</div>
{:then questions}
<SurveyQuestions {survey} {questions} />
{:catch error}
<div class="row mt-5">
<div class="d-none d-sm-block col-sm">
<hr>
</div>
<h3 class="col-sm-auto text-center text-muted mb-3"><label for="askquestion">Ce questionnaire n'est pas accessible</label></h3>
<div class="d-none d-sm-block col-sm">
<hr>
</div>
</div>
{#if survey.direct != null}
<div class="alert alert-warning">
<strong><a href="surveys/{survey.id}/live">Cliquez ici pour accéder au direct</a>.</strong> Il s'agit d'un questionnaire en direct, le questionnaire n'est pas accessible sur cette page.
</div>
{/if}
{/await}
{/await}

View File

@ -0,0 +1,8 @@
export async function load({ parent, params }) {
const stuff = await parent();
return {
survey: stuff.survey,
sid: params.sid,
};
}

View File

@ -1,14 +1,3 @@
<script context="module">
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
sid: params.sid,
},
};
}
</script>
<script>
import { user } from '$lib/stores/user';
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
@ -21,28 +10,24 @@
import { getQuestion, getQuestions, Question } from '$lib/questions';
import { getUsers } from '$lib/users';
export let surveyP;
export let sid;
export let data;
let survey;
let req_questions;
surveyP.then((s) => {
survey = s;
$: {
survey = data.survey;
updateQuestions();
if (survey.direct !== null) {
wsconnect();
}
});
}
function updateSurvey() {
surveyP = getSurvey(survey.id);
surveyP.then((s) => {
survey = s;
updateQuestions();
if (survey.direct !== null) {
wsconnect();
}
});
async function updateSurvey() {
survey = await getSurvey(survey.id);
updateQuestions();
if (survey.direct !== null) {
wsconnect();
}
}
function updateQuestions() {
@ -162,7 +147,7 @@
function wsconnect() {
if (ws !== null) return;
ws = new WebSocket((window.location.protocol == 'https:'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws-admin`);
ws = new WebSocket((window.location.protocol == 'https:'?'wss://':'ws://') + window.location.host + `/api/surveys/${data.sid}/ws-admin`);
ws.addEventListener("open", () => {
ws_up = true;
@ -233,472 +218,469 @@
}
</script>
{#await surveyP then survey}
{#if $user && $user.is_admin}
<StartStopLiveSurvey
{survey}
class="ms-1 float-end"
on:update={() => updateSurvey()}
on:end={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
/>
<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>
{#if asks.length}
<a href="surveys/{sid}/admin#questions_part">
<i class="bi bi-patch-question-fill text-danger"></i>
</a>
{/if}
</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 $user && $user.is_admin}
<StartStopLiveSurvey
{survey}
class="ms-1 float-end"
on:update={() => updateSurvey()}
on:end={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
/>
<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>
{#if asks.length}
<a href="surveys/{data.sid}/admin#questions_part">
<i class="bi bi-patch-question-fill text-danger"></i>
</a>
{/if}
</div>
{#if survey.direct === null}
<SurveyAdmin
{survey}
on:saved={updateSurvey}
/>
</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}
{#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>
<SurveyBadge
class="mx-2"
{survey}
/>
{/if}
</div>
{#if survey.direct === null}
<SurveyAdmin
{survey}
on:saved={updateSurvey}
/>
{: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
{#if timer_end}
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
<input
type="number"
class="form-control"
disabled
value={timer_remain}
>
<span class="input-group-text">s</span>
</div>
{:else}
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
<input
type="number"
class="form-control"
bind:value={timer}
placeholder="Valeur du timer"
>
<span class="input-group-text">s</span>
</div>
{/if}
<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"}')} }
title="Passer sur une scène sans question"
>
<i class="bi bi-pause-fill"></i>
</button>
<button
type="button"
class="btn btn-sm"
class:btn-outline-success={!next_corrected}
class:btn-success={next_corrected}
on:click={() => { next_corrected = !next_corrected } }
title="La prochaine question est affichée corrigée"
>
<i class="bi bi-eye"></i>
</button>
<button
type="button"
class="btn btn-sm btn-info"
on:click={() => { edit_question = new Question({ id_survey: survey.id }) } }
title="Ajouter une question"
>
<i class="bi bi-plus"></i>
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger"
on:click={() => { fetch('api/cache', {method: 'DELETE'}) } }
title="Vider les caches"
>
<i class="bi bi-bandaid-fill"></i>
</button>
</th>
</tr>
</thead>
<tbody>
{#each questions as question (question.id)}
<tr>
<th>
Question
{#if timer_end}
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
<input
type="number"
class="form-control"
disabled
value={timer_remain}
>
<span class="input-group-text">s</span>
</div>
<td>
{#if responses[question.id]}
<a href="surveys/{data.sid}/admin#q{question.id}_res">
{question.title}
</a>
{:else}
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
<input
type="number"
class="form-control"
bind:value={timer}
placeholder="Valeur du timer"
>
<span class="input-group-text">s</span>
</div>
{question.title}
{/if}
<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"}')} }
title="Passer sur une scène sans question"
>
<i class="bi bi-pause-fill"></i>
</button>
</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"
class:btn-outline-success={!next_corrected}
class:btn-primary={!next_corrected}
class:btn-success={next_corrected}
on:click={() => { next_corrected = !next_corrected } }
title="La prochaine question est affichée corrigée"
disabled={(question.id === current_question && next_corrected == corrected) || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "corrected": ' + next_corrected + ', "timer": 0, "question":' + question.id + '}')} }
>
<i class="bi bi-eye"></i>
<i class="bi bi-play-fill"></i>
</button>
<button
type="button"
class="btn btn-sm btn-danger"
disabled={question.id === current_question || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "corrected": ' + next_corrected + ', "timer": ' + timer * 1000 + ',"question":' + question.id + '}')} }
>
<i class="bi bi-stopwatch-fill"></i>
</button>
<a
href="/surveys/{survey.id}/responses/{question.id}"
target="_blank"
type="button"
class="btn btn-sm btn-success"
>
<i class="bi bi-files"></i>
</a>
<button
type="button"
class="btn btn-sm btn-info"
on:click={() => { edit_question = new Question({ id_survey: survey.id }) } }
title="Ajouter une question"
disabled={question.id === current_question}
on:click={() => { getQuestion(question.id).then((q) => {edit_question = q})} }
>
<i class="bi bi-plus"></i>
<i class="bi bi-pencil"></i>
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger"
on:click={() => { fetch('api/cache', {method: 'DELETE'}) } }
title="Vider les caches"
>
<i class="bi bi-bandaid-fill"></i>
</button>
</th>
</td>
</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"
class:btn-primary={!next_corrected}
class:btn-success={next_corrected}
disabled={(question.id === current_question && next_corrected == corrected) || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "corrected": ' + next_corrected + ', "timer": 0, "question":' + question.id + '}')} }
>
<i class="bi bi-play-fill"></i>
</button>
<button
type="button"
class="btn btn-sm btn-danger"
disabled={question.id === current_question || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "corrected": ' + next_corrected + ', "timer": ' + timer * 1000 + ',"question":' + question.id + '}')} }
>
<i class="bi bi-stopwatch-fill"></i>
</button>
<a
href="/surveys/{survey.id}/responses/{question.id}"
target="_blank"
type="button"
class="btn btn-sm btn-success"
>
<i class="bi bi-files"></i>
</a>
<button
type="button"
class="btn btn-sm btn-info"
disabled={question.id === current_question}
on:click={() => { getQuestion(question.id).then((q) => {edit_question = q})} }
>
<i class="bi bi-pencil"></i>
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{#if edit_question !== null}
<QuestionForm
{survey}
edit
question={edit_question}
on:delete={() => deleteQuestion(edit_question)}
/>
{/if}
{/each}
</tbody>
</table>
</div>
{/await}
{#if edit_question !== null}
<QuestionForm
{survey}
edit
question={edit_question}
on:delete={() => deleteQuestion(edit_question)}
/>
{/if}
<hr>
<button
type="button"
class="btn btn-sm btn-info ms-1 float-end"
on:click={() => { ws.send('{"action":"get_asks", "value": ""}'); asks = []; }}
title="Rafraîchir les réponses"
>
<i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-question-diamond"></i>
</button>
<button
type="button"
class="btn btn-sm btn-light ms-1 float-end"
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
>
<i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-question-diamond"></i>
</button>
<button
type="button"
class="btn btn-sm btn-success float-end"
title="Tout marqué comme répondu"
on:click={() => { ws.send('{"action":"mark_answered", "value": "all"}'); asks = [] }}
>
<i class="bi bi-check-all"></i>
</button>
<h3 id="questions_part">
Questions
{#if asks.length}
<small class="text-muted">
{asks.length}&nbsp;question{#if asks.length > 1}s{/if}
</small>
{/if}
</h3>
<hr>
<button
type="button"
class="btn btn-sm btn-info ms-1 float-end"
on:click={() => { ws.send('{"action":"get_asks", "value": ""}'); asks = []; }}
title="Rafraîchir les réponses"
>
<i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-question-diamond"></i>
</button>
<button
type="button"
class="btn btn-sm btn-light ms-1 float-end"
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
>
<i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-question-diamond"></i>
</button>
<button
type="button"
class="btn btn-sm btn-success float-end"
title="Tout marqué comme répondu"
on:click={() => { ws.send('{"action":"mark_answered", "value": "all"}'); asks = [] }}
>
<i class="bi bi-check-all"></i>
</button>
<h3 id="questions_part">
Questions
{#if asks.length}
{#each asks as ask (ask.id)}
<div class="card mb-3">
<div class="card-body">
<p class="card-text">
{ask.content}
</p>
</div>
<div class="card-footer">
<button
type="button"
class="btn btn-sm btn-success float-end"
title="Marqué comme répondu"
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
>
<i class="bi bi-check"></i>
</button>
Par
<a href="users/{ask.userid}" target="_blank">
{#if users && users[ask.userid]}
{users[ask.userid].login}
{:else}
{ask.userid}
{/if}
</a>
<small class="text-muted">
{asks.length}&nbsp;question{#if asks.length > 1}s{/if}
</small>
{/if}
</h3>
{#if asks.length}
{#each asks as ask (ask.id)}
<div class="card mb-3">
<div class="card-body">
<p class="card-text">
{ask.content}
</p>
</div>
<div class="card-footer">
<button
type="button"
class="btn btn-sm btn-success float-end"
title="Marqué comme répondu"
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
>
<i class="bi bi-check"></i>
</button>
Par
<a href="users/{ask.userid}" target="_blank">
{#if users && users[ask.userid]}
{users[ask.userid].login}
{:else}
{ask.userid}
{/if}
</a>
</div>
</div>
{/each}
{:else}
<div class="text-center text-muted">
Pas de question pour l'instant.
</div>
{/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}
{#if current_question == question.id}
<CorrectionPieChart
{question}
{proposals}
data={graph_data}
/>
{:else}
<CorrectionPieChart
{question}
/>
{/if}
<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}
{#if current_question == question.id}
<CorrectionPieChart
{question}
{proposals}
data={graph_data}
/>
{:else}
<CorrectionPieChart
{question}
/>
{/if}
<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 if question.kind && question.kind.startsWith('list')}
<ListInputResponses
responses={responses[q]}
{users}
/>
{: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>
<button
type="button"
class="btn btn-sm btn-warning ms-1 float-end"
on:click={() => { scroll_states = {}; ws.send('{"action":"where_are_you"}')} }
title="Rapporter l'avancement"
>
<i class="bi bi-geo-fill"></i>
</button>
<h3 id="users">
Connectés
{#if wsstats}
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
{/if}
{#if scroll_mean}
<small
class:text-danger={scroll_mean >= 0 && scroll_mean < 0.2}
class:text-warning={scroll_mean >= 0.2 && scroll_mean < 0.6}
class:text-info={scroll_mean >= 0.6 && scroll_mean < 0.9}
class:text-success={scroll_mean >= 0.9}
>Avancement global&nbsp;: {Math.trunc(scroll_mean*10000)/100} %</small>
{/if}
</h3>
{#if wsstats && wsstats.users}
<div class="row row-cols-5 py-3">
{#each wsstats.users as user, lid (lid)}
<div class="col">
<div class="card">
<img alt="{user.login}" src="//photos.cri.epita.fr/thumb/{user.login}" class="card-img-top">
<div class="card-footer text-center text-truncate p-0">
<a href="users/{user.login}" target="_blank">
{user.login}
</a>
</div>
{#if user.myscroll != null}
<div
class="card-footer py-0 px-1"
class:bg-danger={user.myscroll >= 0 && user.myscroll < 0.2}
class:bg-warning={user.myscroll >= 0.2 && user.myscroll < 0.6}
class:bg-info={user.myscroll >= 0.6 && user.myscroll < 0.9}
class:bg-success={user.myscroll >= 0.9}
>
Avancement&nbsp;: {Math.trunc(user.myscroll*10000)/100}&nbsp;%
</div>
{/if}
</div>
</div>
{/each}
{:else}
<div class="text-center text-muted">
Pas de question pour l'instant.
</div>
{/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}
{#if current_question == question.id}
<CorrectionPieChart
{question}
{proposals}
data={graph_data}
/>
{:else}
<CorrectionPieChart
{question}
/>
{/if}
<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}
{#if current_question == question.id}
<CorrectionPieChart
{question}
{proposals}
data={graph_data}
/>
{:else}
<CorrectionPieChart
{question}
/>
{/if}
<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 if question.kind && question.kind.startsWith('list')}
<ListInputResponses
responses={responses[q]}
{users}
/>
{: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>
<button
type="button"
class="btn btn-sm btn-warning ms-1 float-end"
on:click={() => { scroll_states = {}; ws.send('{"action":"where_are_you"}')} }
title="Rapporter l'avancement"
>
<i class="bi bi-geo-fill"></i>
</button>
<h3 id="users">
Connectés
{#if wsstats}
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
{/if}
{#if scroll_mean}
<small
class:text-danger={scroll_mean >= 0 && scroll_mean < 0.2}
class:text-warning={scroll_mean >= 0.2 && scroll_mean < 0.6}
class:text-info={scroll_mean >= 0.6 && scroll_mean < 0.9}
class:text-success={scroll_mean >= 0.9}
>Avancement global&nbsp;: {Math.trunc(scroll_mean*10000)/100} %</small>
{/if}
</h3>
{#if wsstats && wsstats.users}
<div class="row row-cols-5 py-3">
{#each wsstats.users as user, lid (lid)}
<div class="col">
<div class="card">
<img alt="{user.login}" src="//photos.cri.epita.fr/thumb/{user.login}" class="card-img-top">
<div class="card-footer text-center text-truncate p-0">
<a href="users/{user.login}" target="_blank">
{user.login}
</a>
</div>
{#if user.myscroll != null}
<div
class="card-footer py-0 px-1"
class:bg-danger={user.myscroll >= 0 && user.myscroll < 0.2}
class:bg-warning={user.myscroll >= 0.2 && user.myscroll < 0.6}
class:bg-info={user.myscroll >= 0.6 && user.myscroll < 0.9}
class:bg-success={user.myscroll >= 0.9}
>
Avancement&nbsp;: {Math.trunc(user.myscroll*10000)/100}&nbsp;%
</div>
{/if}
</div>
</div>
{/each}
</div>
{/if}
</div>
{/if}
{/await}
{/if}

View File

@ -0,0 +1,8 @@
export async function load({ params, parent }) {
const stuff = await parent();
return {
survey: stuff.survey,
sid: params.sid,
};
}

View File

@ -1,14 +1,3 @@
<script context="module">
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
sid: params.sid,
},
};
}
</script>
<script>
import { onDestroy } from 'svelte';
@ -18,11 +7,9 @@
import QuestionForm from '$lib/components/QuestionForm.svelte';
import { getQuestion } from '$lib/questions';
export let surveyP;
export let sid;
export let data;
let survey;
surveyP.then((s) => survey = s);
$: survey = data.survey;
let ws_up = false;
let show_question = null;
@ -75,7 +62,7 @@
}
});
function wsconnect() {
ws = new WebSocket((window.location.protocol == 'https:'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`);
ws = new WebSocket((window.location.protocol == 'https:'?'wss://':'ws://') + window.location.host + `/api/surveys/${data.sid}/ws`);
ws.addEventListener("open", () => {
ws_up = true;
@ -183,120 +170,118 @@
let corrections = null;
</script>
{#await surveyP then unused}
<div
style={"transition: opacity 150ms ease-out; opacity: " + (displaySendInProgress?1:0)}
class="ms-2 float-end"
>
<div style="position: relative; left: 25%; top: 4px">
<div style="position: absolute">
<i class="bi bi-save"></i>
</div>
</div>
<div class="spinner-border text-primary" role="status"></div>
</div>
{#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-3 mb-md-4 mb-lg-5">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
</h2>
<div
style={"transition: opacity 150ms ease-out; opacity: " + (displaySendInProgress?1:0)}
class="ms-2 float-end"
class="badge rounded-pill ms-2"
class:bg-success={ws_up}
class:bg-danger={!ws_up}
>
<div style="position: relative; left: 25%; top: 4px">
<div style="position: absolute">
<i class="bi bi-save"></i>
</div>
</div>
<div class="spinner-border text-primary" role="status"></div>
</div>
{#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-3 mb-md-4 mb-lg-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>
{#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 &hellip;</span>
</div>
{:then question}
<QuestionForm
{survey}
{question}
readonly={timer >= 100 || survey.corrected}
{corrections}
bind:value={value}
on:change={sendValue}
<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 &hellip;</span>
</div>
{:then question}
<QuestionForm
{survey}
{question}
readonly={timer >= 100 || survey.corrected}
{corrections}
bind:value={value}
on:change={sendValue}
>
{#if timer_init}
<div class="progress" style="border-radius: 0; height: 4px">
<div class="progress-bar" class:bg-warning={timer > 85 && timer < 100} class:bg-danger={timer >= 100} role="progressbar" style="width: {timer}%"></div>
</div>
{/if}
</QuestionForm>
{#if question.kind != 'mcq' && question.kind != 'ucq' && question.kind != 'none'}
<button
class="btn btn-primary"
>
{#if timer_init}
<div class="progress" style="border-radius: 0; height: 4px">
<div class="progress-bar" class:bg-warning={timer > 85 && timer < 100} class:bg-danger={timer >= 100} role="progressbar" style="width: {timer}%"></div>
</div>
{/if}
</QuestionForm>
{#if question.kind != 'mcq' && question.kind != 'ucq' && question.kind != 'none'}
Soumettre cette réponse
</button>
{/if}
{/await}
{:else if ws_up}
<h2 class="text-center mb-4">
Pas de question actuellement.
</h2>
<form on:submit|preventDefault={askQuestion}>
<div class="row">
<div class="d-none d-sm-block col-sm">
<hr>
</div>
<h3 class="col-sm-auto text-center text-muted mb-3"><label for="askquestion">Vous avez une question&nbsp;?</label></h3>
<div class="d-none d-sm-block col-sm">
<hr>
</div>
</div>
<div class="row">
<div class="offset-md-1 col-md-10 offset-lg-2 col-lg-8 offset-xl-3 col-xl-6 mb-4">
<div class="input-group">
<textarea
id="askquestion"
class="form-control"
bind:value={myQuestion}
autofocus
placeholder="Remarques, soucis, choses pas claires? Levez la main ou écrivez ici!"
></textarea>
<button
class="d-sm-none btn btn-primary"
disabled={!myQuestion || submitQuestionInProgress}
>
{#if submitQuestionInProgress}
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
{/if}
Poser cette question
</button>
</div>
</div>
</div>
{#if myQuestion}
<div class="d-none d-sm-block text-center mb-4">
<button
class="btn btn-primary"
disabled={submitQuestionInProgress}
>
Soumettre cette réponse
{#if submitQuestionInProgress}
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
{/if}
Poser cette question
</button>
{/if}
{/await}
{:else if ws_up}
<h2 class="text-center mb-4">
Pas de question actuellement.
</h2>
<form on:submit|preventDefault={askQuestion}>
<div class="row">
<div class="d-none d-sm-block col-sm">
<hr>
</div>
<h3 class="col-sm-auto text-center text-muted mb-3"><label for="askquestion">Vous avez une question&nbsp;?</label></h3>
<div class="d-none d-sm-block col-sm">
<hr>
</div>
</div>
<div class="row">
<div class="offset-md-1 col-md-10 offset-lg-2 col-lg-8 offset-xl-3 col-xl-6 mb-4">
<div class="input-group">
<textarea
id="askquestion"
class="form-control"
bind:value={myQuestion}
autofocus
placeholder="Remarques, soucis, choses pas claires? Levez la main ou écrivez ici!"
></textarea>
<button
class="d-sm-none btn btn-primary"
disabled={!myQuestion || submitQuestionInProgress}
>
{#if submitQuestionInProgress}
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
{/if}
Poser cette question
</button>
</div>
</div>
</div>
{#if myQuestion}
<div class="d-none d-sm-block text-center mb-4">
<button
class="btn btn-primary"
disabled={submitQuestionInProgress}
>
{#if submitQuestionInProgress}
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
{/if}
Poser cette question
</button>
</div>
{/if}
</form>
{: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}
{/if}
</form>
{: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>

View File

@ -0,0 +1,7 @@
export async function load({ parent }) {
const stuff = await parent();
return {
survey: stuff.survey,
};
}

View File

@ -1,32 +1,20 @@
<script context="module">
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
import { getSurvey } from '$lib/surveys';
import { getUsers } from '$lib/users';
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
},
};
}
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '$lib/stores/user';
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
import StartStopLiveSurvey from '$lib/components/StartStopLiveSurvey.svelte';
import SurveyAdmin from '$lib/components/SurveyAdmin.svelte';
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
import SurveyQuestions from '$lib/components/SurveyQuestions.svelte';
import { getQuestions } from '$lib/questions';
import { getUsers } from '$lib/users';
export let surveyP;
let usersP = null;
surveyP.then((s) => {
usersP = getUsers(s.promo, s.group);
})
export let data;
let survey;
let usersP;
$: survey = data.survey;
$: usersP = getUsers(data.survey.promo, data.survey.group);
let edit = false;
let exportview = false;
let exportview_list = false;
@ -41,122 +29,120 @@
}
</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>
<StartStopLiveSurvey
{survey}
class="ms-1 float-end"
on:update={() => goto(`surveys/${survey.id}/admin`)}
/>
<button
class="btn btn-outline-dark ms-1 float-end"
title="Partager les résultats"
on:click={() => shareResults(survey)}
><i class="bi bi-share-fill"></i></button>
{#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>
<StartStopLiveSurvey
{survey}
class="ms-1 float-end"
on:update={() => goto(`surveys/${survey.id}/admin`)}
/>
<button
class="btn btn-outline-dark ms-1 float-end"
title="Partager les résultats"
on:click={() => shareResults(survey)}
><i class="bi bi-share-fill"></i></button>
<button
class="btn ms-1 float-end"
class:btn-dark={exportview}
class:btn-outline-dark={!exportview}
title="Afficher les graphiques"
on:click={() => { exportview = !exportview; } }
><i class="bi bi-activity"></i></button>
{#if exportview}
<button
class="btn ms-1 float-end"
class:btn-dark={exportview}
class:btn-outline-dark={!exportview}
title="Afficher les graphiques"
on:click={() => { exportview = !exportview; } }
><i class="bi bi-activity"></i></button>
{#if exportview}
<button
class="btn ms-1 float-end"
class:btn-dark={exportview_list}
class:btn-outline-dark={!exportview_list}
title="Traiter les listes comme des textes : ne pas chercher à les regrouper sous un diagramme"
on:click={() => { exportview_list = !exportview_list; } }
><i class="bi bi-view-list"></i></button>
{/if}
class:btn-dark={exportview_list}
class:btn-outline-dark={!exportview_list}
title="Traiter les listes comme des textes : ne pas chercher à les regrouper sous un diagramme"
on:click={() => { exportview_list = !exportview_list; } }
><i class="bi bi-view-list"></i></button>
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/{survey.id}" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
<small class="text-muted">{#if exportview}Réponses{:else}Corrections{/if}</small>
</h2>
<SurveyBadge class="ms-2" {survey} />
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/{survey.id}" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
<small class="text-muted">{#if exportview}Réponses{:else}Corrections{/if}</small>
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
{#if $user && $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 &hellip;</span>
</div>
{#if $user && $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 &hellip;</span>
</div>
{:then questions}
{#if !exportview}
<div class="card mt-3 mb-5">
<table class="table table-hover table-striped mb-0">
<thead>
{:then questions}
{#if !exportview}
<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>
<th>Question</th>
<th>Réponses</th>
<th>Moyenne</th>
<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 &hellip;</span>
</td>
{:then responses}
<td>
{#if responses}
{responses.filter((r) => !r.time_scored || (r.time_reported && r.time_reported >= r.time_scored)).length} /
{responses.length}
{#await usersP then users}
<br>
{Math.trunc(responses.length/users.length*1000)/10}&nbsp;%
{/await}
{:else}
0
{/if}
</td>
<td>
{#if responses && responses.filter((r) => r.time_scored).length}
{Math.trunc(responses.reduce((p, c) => (p + (c.score?c.score:0)), 0)/responses.filter((r) => r.time_scored).length*10)/10}&nbsp;%
{:else}
--&nbsp;%
{/if}
</td>
{/await}
</tr>
</thead>
<tbody ng-controller="SurveyGradesController">
{#each questions as question (question.id)}
<tr>
<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 &hellip;</span>
</td>
{:then responses}
<td>
{#if responses}
{responses.filter((r) => !r.time_scored || (r.time_reported && r.time_reported >= r.time_scored)).length} /
{responses.length}
{#await usersP then users}
<br>
{Math.trunc(responses.length/users.length*1000)/10}&nbsp;%
{/await}
{:else}
0
{/if}
</td>
<td>
{#if responses && responses.filter((r) => r.time_scored).length}
{Math.trunc(responses.reduce((p, c) => (p + (c.score?c.score:0)), 0)/responses.filter((r) => r.time_scored).length*10)/10}&nbsp;%
{:else}
--&nbsp;%
{/if}
</td>
{/await}
</tr>
{/each}
</tbody>
</table>
</div>
{:else}
{#each questions as question (question.id)}
<h3>{question.title}</h3>
{#if question.kind == "text" || (exportview_list && question.kind.indexOf("list") == 0)}
{#await question.getResponses() then responses}
{#each responses as response (response.id)}
<div class="card mb-2">
<div class="card-body">
<p class="card-text" style:white-space="pre-line">
{response.value}
</p>
</div>
{/each}
</tbody>
</table>
</div>
{:else}
{#each questions as question (question.id)}
<h3>{question.title}</h3>
{#if question.kind == "text" || (exportview_list && question.kind.indexOf("list") == 0)}
{#await question.getResponses() then responses}
{#each responses as response (response.id)}
<div class="card mb-2">
<div class="card-body">
<p class="card-text" style:white-space="pre-line">
{response.value}
</p>
</div>
{/each}
{/await}
{:else}
<CorrectionPieChart {question} />
{/if}
<hr class="mb-3">
{/each}
{/if}
{/await}
</div>
{/each}
{/await}
{:else}
<CorrectionPieChart {question} />
{/if}
<hr class="mb-3">
{/each}
{/if}
{/await}
<div class="modal fade" tabindex="-1" id="shareModal">

View File

@ -0,0 +1,8 @@
export async function load({ params, parent }) {
const stuff = await parent();
return {
survey: stuff.survey,
rid: params.rid,
};
}

View File

@ -1,16 +1,3 @@
<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 '$lib/components/Correction.svelte';
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
@ -20,8 +7,7 @@
import { getCorrectionTemplates } from '$lib/correctionTemplates';
import { getQuestion } from '$lib/questions';
export let surveyP;
export let rid;
export let data;
let showChart = false;
let showResponses = false;
@ -31,152 +17,156 @@
let child;
let waitApply = false;
let ctpls = getCorrectionTemplates(rid);
let filter = "";
let ctpls;
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;
}
$: {
ctpls = getCorrectionTemplates(data.rid);
if (ctpls) {
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;
});
}
let filter = "";
let nodescription = false;
let y = 0;
$: nodescription = y > 10;
let survey = null;
$: survey = data.survey;
</script>
<svelte:window bind:scrollY={y} />
{#await surveyP then survey}
{#await getQuestion(rid)}
{#await getQuestion(data.rid)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement de la question&hellip;</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&hellip;</span>
<span>Chargement des templates&hellip;</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">&lt;</a>
{survey.title}
<small class="text-muted">Corrections</small>
</h2>
<SurveyBadge class="ms-2" {survey} />
</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&hellip;</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">&lt;</a>
{survey.title}
<small class="text-muted">Corrections</small>
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
<div class="card sticky-top" style="overflow-y: auto; max-height: 70vh">
<QuestionHeader
{question}
nodescription={nodescription}
>
<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}
{#if question.kind == "ucq" || question.kind == "mcq" || question.kind == "int"}
<button
type="button"
class="btn btn-sm float-end mx-1"
class:btn-outline-info={!showChart}
class:btn-info={showChart}
on:click={() => showChart = !showChart}
title="Afficher les résultats"
>
<i class="bi bi-bar-chart-line-fill"></i>
</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}
{#if showChart}
<CorrectionPieChart
{question}
class="card-body"
/>
{/if}
</div>
<Correction
{cts}
{filter}
<div class="card sticky-top" style="overflow-y: auto; max-height: 70vh">
<QuestionHeader
{question}
{showStudent}
{notCorrected}
bind:child={child}
templates={correctionTemplates}
on:nb_responses={(v) => { nb_responses = v.detail; } }
/>
{/await}
nodescription={nodescription}
>
<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}
{#if question.kind == "ucq" || question.kind == "mcq" || question.kind == "int"}
<button
type="button"
class="btn btn-sm float-end mx-1"
class:btn-outline-info={!showChart}
class:btn-info={showChart}
on:click={() => showChart = !showChart}
title="Afficher les résultats"
>
<i class="bi bi-bar-chart-line-fill"></i>
</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}
{#if showChart}
<CorrectionPieChart
{question}
class="card-body"
/>
{/if}
</div>
<Correction
{cts}
{filter}
{question}
{showStudent}
{notCorrected}
bind:child={child}
templates={correctionTemplates}
on:nb_responses={(v) => { nb_responses = v.detail; } }
/>
{/await}
{/await}

View File

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
uid: params.uid,
};
}

View File

@ -1,13 +1,3 @@
<script context="module">
export async function load({ params }) {
return {
props: {
uid: params.uid,
}
};
}
</script>
<script lang="ts">
import UserKeys from '$lib/components/UserKeys.svelte';
import UserSurveys from '$lib/components/UserSurveys.svelte';
@ -15,13 +5,13 @@
import { getSurveys } from '$lib/surveys';
import { getUser, getUserGrade, getUserScore } from '$lib/users';
export let uid;
export let data;
let allPromos = false;
let myuser = null;
let userP = null;
$: userP = getUser(uid).then((u) => myuser = u)
$: userP = getUser(data.uid).then((u) => myuser = u)
function impersonate() {
fetch('api/auth/impersonate', {

View File

@ -0,0 +1,5 @@
export async function load({ params }) {
return {
uid: params.uid,
};
}

View File

@ -1,25 +1,15 @@
<script context="module">
export async function load({ params }) {
return {
props: {
uid: params.uid,
}
};
}
</script>
<script lang="ts">
import UserSurveys from '$lib/components/UserSurveys.svelte';
import { user } from '$lib/stores/user';
import { getSurveys } from '$lib/surveys';
import { getUser, getUserGrade, getUserScore } from '$lib/users';
export let uid;
export let data;
let allPromos = false;
</script>
{#await getUser(uid)}
{#await getUser(data.uid)}
<h2>
Étudiant
</h2>

View File

@ -0,0 +1,6 @@
export async function load({ params }) {
return {
sid: params.sid,
uid: params.uid,
};
}

View File

@ -1,14 +1,3 @@
<script context="module">
export async function load({ params }) {
return {
props: {
sid: params.sid,
uid: params.uid,
}
};
}
</script>
<script lang="ts">
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
import SurveyQuestions from '$lib/components/SurveyQuestions.svelte';
@ -16,11 +5,10 @@
import { getQuestions } from '$lib/questions';
import { getUser } from '$lib/users';
export let sid;
export let uid;
export let data;
</script>
{#await getUser(uid)}
{#await getUser(data.uid)}
<h2>
Étudiant
</h2>
@ -30,7 +18,7 @@
Chargement des d&eacute;tails&hellip;
</div>
{:then student}
{#await getSurvey(sid)}
{#await getSurvey(data.sid)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement du questionnaire &hellip;</span>
@ -50,7 +38,7 @@
<span>Chargement des questions &hellip;</span>
</div>
{:then questions}
<SurveyQuestions {survey} {questions} id_user={uid} />
<SurveyQuestions {survey} {questions} id_user={data.uid} />
{/await}
{:catch error}
<div class="text-center">

View File

@ -0,0 +1,9 @@
import { getWork } from '$lib/works';
export async function load({ params }) {
const work = getWork(params.wid);
return {
work,
};
}

View File

@ -1,27 +1,8 @@
<script context="module">
import { getWork } from '$lib/works';
export async function load({ params, stuff }) {
const work = getWork(params.wid);
return {
props: {
work,
},
stuff: {
...stuff,
work,
}
};
}
</script>
<script lang="ts">
export let work;
export let data;
</script>
{#await work}
{#await data.work}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement du rendu &hellip;</span>

View File

@ -0,0 +1,7 @@
export async function load({ parent }) {
const stuff = await parent();
return {
work: stuff.work,
};
}

View File

@ -1,15 +1,3 @@
<script context="module">
import { getWork } from '$lib/works';
export async function load({ params, stuff }) {
return {
props: {
work: stuff.work,
},
};
}
</script>
<script lang="ts">
import { goto } from '$app/navigation';
@ -21,222 +9,220 @@
import WorkRepository from '$lib/components/WorkRepository.svelte';
import { getScore } from '$lib/users';
export let work = null;
export let data;
let edit = false;
let my_submission = null;
let warn_already_used = false;
let w = null;
work.then((w) => {
refresh_submission(w);
})
$: w = data.work;
$: refresh_submission(data.work);
function refresh_submission(w) {
my_submission = w.getSubmission();
}
</script>
{#await work then w}
{#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 class="btn btn-success ms-1 float-end" href="works/{w.id}/rendus" title="Voir les rendus"><i class="bi bi-files"></i></a>
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="works/" class="text-muted" style="text-decoration: none">&lt;</a>
{w.title}
</h2>
<SurveyBadge class="ms-2" survey={w} />
</div>
{#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 class="btn btn-success ms-1 float-end" href="works/{w.id}/rendus" title="Voir les rendus"><i class="bi bi-files"></i></a>
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="works/" class="text-muted" style="text-decoration: none">&lt;</a>
{w.title}
</h2>
<SurveyBadge class="ms-2" survey={w} />
</div>
{#if $user && $user.is_admin && edit}
<WorkAdmin work={w} on:saved={() => edit = false} />
{#if $user && $user.is_admin && edit}
<WorkAdmin work={w} on:saved={() => edit = false} />
{#if w.description}
{#if w.description}
<hr>
{@html w.description}
{/if}
{/if}
<hr>
<h3 class="mt-3">Notes</h3>
<div class="card mt-3 mb-5">
{#await w.getGrades()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des notes &hellip;</span>
</div>
{:then grades}
<table class="table table-hover table-striped mb-0">
<thead>
<tr>
<th>Login</th>
<th>Note</th>
<th>Commentaire</th>
<th>Date de la note</th>
</tr>
</thead>
<tbody>
{#if !grades}
<div class="text-center">
Aucune note n'a encore été envoyée pour ce travail.
</div>
{:else}
{#each grades as grade, gid (grade.id)}
<tr>
<td>
<a href="users/{grade.id_user}">{grade.login}</a>
</td>
<td>{grade.score}</td>
<td>{#if grade.comment}{grade.comment}{:else}-{/if}</td>
<td>{grade.date}</td>
</tr>
{/each}
{/if}
</tbody>
</table>
{/await}
</div>
{:else if (!$user || !$user.is_admin) && new Date(w.start_availability) > new Date()}
<div class="alert alert-warning">
<i class="bi bi-stopwatch-fill"></i>
<strong>Ce travail n'est pas encore ouvert.</strong> Revenez plus tard&nbsp;!
</div>
{:else}
<dl style="columns: 3">
<dt>Date de début</dt>
<dd><DateFormat date={new Date(w.start_availability)} dateStyle="medium" timeStyle="medium" /></dd>
<dt>Date de fin</dt>
<dd><DateFormat date={new Date(w.end_availability)} dateStyle="medium" timeStyle="medium" /></dd>
{#if w.submission_url != "-"}
<dt>Rendu&nbsp;?</dt>
<dd>
{#if w.submission_url}
<SubmissionStatus work={w} user={$user} />
<hr>
<h3 class="mt-3">Notes</h3>
<div class="card mt-3 mb-5">
{#await w.getGrades()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des notes &hellip;</span>
</div>
{:then grades}
<table class="table table-hover table-striped mb-0">
<thead>
<tr>
<th>Login</th>
<th>Note</th>
<th>Commentaire</th>
<th>Date de la note</th>
</tr>
</thead>
<tbody>
{#if !grades}
<div class="text-center">
Aucune note n'a encore été envoyée pour ce travail.
</div>
{:else}
{#each grades as grade, gid (grade.id)}
<tr>
<td>
<a href="users/{grade.id_user}">{grade.login}</a>
</td>
<td>{grade.score}</td>
<td>{#if grade.comment}{grade.comment}{:else}-{/if}</td>
<td>{grade.date}</td>
</tr>
{/each}
{/if}
</tbody>
</table>
{/await}
</div>
{:else if (!$user || !$user.is_admin) && new Date(w.start_availability) > new Date()}
<div class="alert alert-warning">
<i class="bi bi-stopwatch-fill"></i>
<strong>Ce travail n'est pas encore ouvert.</strong> Revenez plus tard&nbsp;!
</div>
{:else}
<dl style="columns: 3">
<dt>Date de début</dt>
<dd><DateFormat date={new Date(w.start_availability)} dateStyle="medium" timeStyle="medium" /></dd>
<dt>Date de fin</dt>
<dd><DateFormat date={new Date(w.end_availability)} dateStyle="medium" timeStyle="medium" /></dd>
{#if w.submission_url != "-"}
<dt>Rendu&nbsp;?</dt>
<dd>
{#if w.submission_url}
<SubmissionStatus work={w} user={$user} />
{:else}
{#await my_submission}
<div class="spinner-grow spinner-grow-sm mx-1" role="status"></div>
{:then submission}
<i
class="bi bi-check-circle text-success"
title="Oui !"
></i>
<DateFormat date={new Date(submission.date)} dateStyle="medium" timeStyle="medium" />
{:catch}
<i
class="bi bi-x-circle text-danger"
title="Pas de rendu trouvé"
></i>
Non
{/await}
{/if}
</dd>
{/if}
</dl>
{#if w.description}
<hr>
{@html w.description}
{/if}
<hr>
<h3 class="mt-3">Rendu</h3>
{#if !w.corrected && w.submission_url != "-"}
<p>
Pour rendre votre travail, vous devez préalablement créer un dépôt Git sur la <a href="https://gitlab.cri.epita.fr/" target="_blank">forge de l'école</a>.<br>Ce dépôt DOIT&nbsp;:
</p>
<ul>
<li>être dans l'espace de nom de votre utilisateur (à la fin de la liste des <span class="fst-italic">namespaces</span>),</li>
<li>avoir la visibilité «&nbsp;Privé&nbsp;»,</li>
<li>avoir invité <a href="https://gitlab.cri.epita.fr/nemunaire" target="_blank" style="font-family: monospace">nemunaire</a> avec le rôle <span class="fst-italic">Reporter</span> une fois le dépôt créé.</li>
</ul>
{#if w.tag}
<div class="alert alert-info">
<i class="bi bi-lightbulb-fill text-info me-1"></i>
Vous pouvez utiliser un dépôt pour tous les travaux à effectuer, ou créer un dépôt par travail.
Son nom/slug n'a pas d'importance car c'est à vous de le sélectionner dans le formulaire ci-après.
</div>
{/if}
{/if}
{#if w.submission_url != "-"}
<WorkRepository class="mb-3" readonly={w.corrected || new Date(w.end_availability) <= new Date()} work={w} on:update_submission={() => refresh_submission(w)} bind:already_used={warn_already_used} />
<div class="card mb-3">
<div class="card-body d-flex justify-content-between">
<div>
{#if w.submission_url}
<strong>État du rendu&nbsp;:</strong> <SubmissionStatus work={w} user={$user} />
{:else}
Rendu&nbsp;:
{#await my_submission}
<div class="spinner-grow spinner-grow-sm mx-1" role="status"></div>
{:then submission}
<i
class="bi bi-check-circle text-success"
title="Oui !"
></i>
<DateFormat date={new Date(submission.date)} dateStyle="medium" timeStyle="medium" />
<strong>{submission.commit} ({submission.tag})</strong> (taille&nbsp;: {submission.size}&nbsp;o, date&nbsp;: <DateFormat date={new Date(submission.date)} dateStyle="medium" timeStyle="medium" />)
{:catch}
<i
class="bi bi-x-circle text-danger"
title="Pas de rendu trouvé"
></i>
Non
<strong>-</strong>
{/await}
{/if}
</dd>
{/if}
</dl>
{#if w.description}
<hr>
{@html w.description}
{/if}
<hr>
<h3 class="mt-3">Rendu</h3>
{#if !w.corrected && w.submission_url != "-"}
<p>
Pour rendre votre travail, vous devez préalablement créer un dépôt Git sur la <a href="https://gitlab.cri.epita.fr/" target="_blank">forge de l'école</a>.<br>Ce dépôt DOIT&nbsp;:
</p>
<ul>
<li>être dans l'espace de nom de votre utilisateur (à la fin de la liste des <span class="fst-italic">namespaces</span>),</li>
<li>avoir la visibilité «&nbsp;Privé&nbsp;»,</li>
<li>avoir invité <a href="https://gitlab.cri.epita.fr/nemunaire" target="_blank" style="font-family: monospace">nemunaire</a> avec le rôle <span class="fst-italic">Reporter</span> une fois le dépôt créé.</li>
</ul>
{#if w.tag}
<div class="alert alert-info">
<i class="bi bi-lightbulb-fill text-info me-1"></i>
Vous pouvez utiliser un dépôt pour tous les travaux à effectuer, ou créer un dépôt par travail.
Son nom/slug n'a pas d'importance car c'est à vous de le sélectionner dans le formulaire ci-après.
</div>
{/if}
{/if}
{#if w.submission_url != "-"}
<WorkRepository class="mb-3" readonly={w.corrected || new Date(w.end_availability) <= new Date()} work={w} on:update_submission={() => refresh_submission(w)} bind:already_used={warn_already_used} />
<div class="card mb-3">
<div class="card-body d-flex justify-content-between">
<div>
{#if w.submission_url}
<strong>État du rendu&nbsp;:</strong> <SubmissionStatus work={w} user={$user} />
{:else}
Rendu&nbsp;:
{#await my_submission}
<div class="spinner-grow spinner-grow-sm mx-1" role="status"></div>
{:then submission}
<strong>{submission.commit} ({submission.tag})</strong> (taille&nbsp;: {submission.size}&nbsp;o, date&nbsp;: <DateFormat date={new Date(submission.date)} dateStyle="medium" timeStyle="medium" />)
{:catch}
<strong>-</strong>
{/await}
{/if}
</div>
<div class="d-flex flex-column justify-content-center">
{#await my_submission then submission}
<a
href="/api/works/{w.id}/download"
class="btn btn-sm btn-dark"
title="Voir la tarball de mon rendu"
>
<i class="bi bi-download"></i>
</a>
{:catch}
<i
class="bi bi-x-circle text-danger"
title="Pas de rendu trouvé"
></i>
{/await}
</div>
<div class="d-flex flex-column justify-content-center">
{#await my_submission then submission}
<a
href="/api/works/{w.id}/download"
class="btn btn-sm btn-dark"
title="Voir la tarball de mon rendu"
>
<i class="bi bi-download"></i>
</a>
{:catch}
<i
class="bi bi-x-circle text-danger"
title="Pas de rendu trouvé"
></i>
{/await}
</div>
</div>
{/if}
</div>
{/if}
{#if w.corrected}
{#await getScore(w)}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then grade}
<div class="alert alert-info">
<i class="bi bi-clipboard2-check-fill text-info me-1"></i>
<strong>Note finale&nbsp;:</strong> <span title="Établie le {grade.date}">{grade.score}</span> {#if grade.comment}&ndash; {grade.comment}{/if}
</div>
{:catch error}
<div class="alert alert-danger">
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
<strong>{error.message}</strong>
</div>
{/await}
{:else if w.submission_url != "-"}
{#if warn_already_used}
<div class="alert alert-danger">
<strong>Vous avez déjà utilisé ce dépôt pour rendre un autre travail.</strong> Pour conserver ce que vous avez fait, tout en respectant l'arborescence de rendu attendue, vous devriez partir d'une nouvelle branche vide&nbsp;:
<pre class="mx-2 mt-1 mb-2">
{#if w.corrected}
{#await getScore(w)}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then grade}
<div class="alert alert-info">
<i class="bi bi-clipboard2-check-fill text-info me-1"></i>
<strong>Note finale&nbsp;:</strong> <span title="Établie le {grade.date}">{grade.score}</span> {#if grade.comment}&ndash; {grade.comment}{/if}
</div>
{:catch error}
<div class="alert alert-danger">
<i class="bi text-warning bi-exclamation-triangle-fill" title={error}></i>
<strong>{error.message}</strong>
</div>
{/await}
{:else if w.submission_url != "-"}
{#if warn_already_used}
<div class="alert alert-danger">
<strong>Vous avez déjà utilisé ce dépôt pour rendre un autre travail.</strong> Pour conserver ce que vous avez fait, tout en respectant l'arborescence de rendu attendue, vous devriez partir d'une nouvelle branche vide&nbsp;:
<pre class="mx-2 mt-1 mb-2">
42sh$ git checkout --orphan renduX
42sh$ git reset
42sh$ rm -r *
# Créez l'arborescence de rendu ensuite</pre>
Pour retrouver ensuite vos rendus des travaux précédents&nbsp;:
<pre class="mx-2 my-1">
Pour retrouver ensuite vos rendus des travaux précédents&nbsp;:
<pre class="mx-2 my-1">
42sh$ git checkout renduY
-- ou --
42sh$ git checkout master
</pre>
</div>
{/if}
<div class="alert alert-warning">
Pour être reconnu, vous devez pousser un tag <strong><a href="keys">signé</a></strong> sur votre dépôt. {#if w.tag}Le tag attendu doit commencer par&nbsp;: <code>{w.tag}</code>. Par exemple <code>{w.tag}v1.0</code>, <code>{w.tag}v1.1</code>, &hellip;{/if} Seul le dernier tag <strong>alphabétique</strong> que vous envoyez avant la date du rendu sera pris en compte. Vous pouvez donc faire autant de tag que vous le souhaitez d'ici la date du rendu.
</div>
{:else}
<div class="alert alert-primary">
Ce travail n'a pas de modalité de rendu.
</pre>
</div>
{/if}
<div class="alert alert-warning">
Pour être reconnu, vous devez pousser un tag <strong><a href="keys">signé</a></strong> sur votre dépôt. {#if w.tag}Le tag attendu doit commencer par&nbsp;: <code>{w.tag}</code>. Par exemple <code>{w.tag}v1.0</code>, <code>{w.tag}v1.1</code>, &hellip;{/if} Seul le dernier tag <strong>alphabétique</strong> que vous envoyez avant la date du rendu sera pris en compte. Vous pouvez donc faire autant de tag que vous le souhaitez d'ici la date du rendu.
</div>
{:else}
<div class="alert alert-primary">
Ce travail n'a pas de modalité de rendu.
</div>
{/if}
{/await}
{/if}
<div class="mb-5"></div>

View File

@ -0,0 +1,7 @@
export async function load({ parent }) {
const stuff = await parent();
return {
work: stuff.work,
};
}

View File

@ -1,15 +1,3 @@
<script context="module">
import { getWork } from '$lib/works';
export async function load({ params, stuff }) {
return {
props: {
work: stuff.work,
},
};
}
</script>
<script lang="ts">
import { goto } from '$app/navigation';
@ -23,11 +11,13 @@
import { ToastsStore } from '$lib/stores/toasts';
import { getUsers } from '$lib/users';
export let work = null;
export let data;
let usersP = null;
let show_dl_btn = { };
let repositoriesP = { };
work.then((w) => {
let w = null;
$: {
w = data.work;
usersP = getUsers(w.promo, w.group);
usersP.then((users) => {
nb_users = users.length;
@ -36,7 +26,7 @@
updateRepoUser(w.id, user.id);
}
});
});
}
function updateRepoUser(wid, userid) {
repositoriesP[userid] = getRepositories(wid, userid);
@ -50,34 +40,33 @@
let search_repo_for = {repo: null, user: null};
</script>
{#await work then w}
<div class="d-flex align-items-center">
<h2>
<a href="works/{w.id}" class="text-muted" style="text-decoration: none">&lt;</a>
{w.title}
<small class="text-muted">Rendus {Math.trunc(nb_rendus/nb_users*100)}&nbsp;% ({nb_rendus}/{nb_users})</small>
</h2>
<SurveyBadge class="ms-2" survey={w} />
</div>
<div class="d-flex align-items-center">
<h2>
<a href="works/{w.id}" class="text-muted" style="text-decoration: none">&lt;</a>
{w.title}
<small class="text-muted">Rendus {Math.trunc(nb_rendus/nb_users*100)}&nbsp;% ({nb_rendus}/{nb_users})</small>
</h2>
<SurveyBadge class="ms-2" survey={w} />
</div>
{#await usersP then users}
<table class="w-100 mb-5">
<thead>
{#await usersP then users}
<table class="w-100 mb-5">
<thead>
<tr>
<th>Login</th>
<th>Rendu</th>
<th>Dépôts</th>
</tr>
</thead>
<tbody>
{#each users as user (user.id)}
<tr>
<th>Login</th>
<th>Rendu</th>
<th>Dépôts</th>
</tr>
</thead>
<tbody>
{#each users as user (user.id)}
<tr>
<td><a href="users/{user.login}">{user.login}</a></td>
<td>
<SubmissionStatus work={w} user={user} on:done={() => { nb_rendus += 1; show_dl_btn[user.id] = true; }} />
</td>
<td>
{#if repositoriesP[user.id]}
<td><a href="users/{user.login}">{user.login}</a></td>
<td>
<SubmissionStatus work={w} user={user} on:done={() => { nb_rendus += 1; show_dl_btn[user.id] = true; }} />
</td>
<td>
{#if repositoriesP[user.id]}
{#await repositoriesP[user.id] then repos}
<div class="d-flex flex-column">
{#each repos as repo (repo.id)}
@ -90,7 +79,7 @@
<DateFormat date={new Date(repo.last_check)} dateStyle="medium" timeStyle="medium" />
<BuildState
repo_pull_state={repo.getBuildState()}
on:show_logs={() => { show_logs = repo.getBuildLogs(user.id); (new bootstrap.Modal(document.getElementById('logsModal'))).show(); }}
on:show_logs={() => { show_logs = repo.getBuildLogs(user.id); (new bootstrap.Modal(document.getElementById('logsModal'))).show(); }}
/>
{:else}
-
@ -115,7 +104,7 @@
</div>
{/each}
</div>
{:catch err}
{:catch err}
<div class="d-flex justify-content-between">
-
<button
@ -127,69 +116,68 @@
</button>
</div>
{/await}
{/if}
</td>
<td>
<a
href="/api/users/{user.id}/works/{w.id}/download"
class="btn btn-sm btn-dark"
class:disabled={!show_dl_btn[user.id]}
title="Télécharger la tarball du rendu"
>
<i class="bi bi-download"></i>
</a>
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
<div class="modal fade" tabindex="-1" id="pullModal">
<div class="modal-dialog">
<form class="modal-content" on:submit|preventDefault={() => {run_pull_for.modal.hide(); try { run_pull_for.repo.retrieveWork(run_pull_for.struct); updateRepoUser(w.id, run_pull_for.user.id); } catch(err) { ToastsStore.addToast({color: "danger", title: "Connexion impossible", msg: err}) };}}>
<div class="modal-header">
<h5 class="modal-title">Repository Pull {#if run_pull_for.user}{run_pull_for.user.login}{/if}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group row mb-2">
<label class="col-2 col-form-label" for="pull-tag">Tag</label>
<input class="form-control col" id="pull-tag" autofocus placeholder={w.tag} bind:value={run_pull_for.struct.tag}>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1" id="optional-sig" bind:checked={run_pull_for.struct.sig_optional}>
<label class="form-check-label" for="optional-sig">
Signature du tag optionnelle
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
Récupérer le travail
</button>
</div>
</form>
</div>
</div>
<div class="modal fade" tabindex="-1" id="repoModal">
<div class="modal-dialog">
<form class="modal-content" on:submit|preventDefault={() => {search_repo_for.modal.hide(); try { search_repo_for.repo.retrieveWork(run_pull_for.struct); updateRepoUser(w.id, run_pull_for.user.id); } catch(err) { ToastsStore.addToast({color: "danger", title: "Connexion impossible", msg: err}) }; search_repo_for.user = null; }}>
<div class="modal-header">
<h5 class="modal-title">Repository selector {#if search_repo_for.user}{search_repo_for.user.login}{/if}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{#if search_repo_for.user}
<WorkRepository work={w} user={search_repo_for.user} />
{/if}
</div>
</form>
</div>
</div>
{/if}
</td>
<td>
<a
href="/api/users/{user.id}/works/{w.id}/download"
class="btn btn-sm btn-dark"
class:disabled={!show_dl_btn[user.id]}
title="Télécharger la tarball du rendu"
>
<i class="bi bi-download"></i>
</a>
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
<div class="modal fade" tabindex="-1" id="pullModal">
<div class="modal-dialog">
<form class="modal-content" on:submit|preventDefault={() => {run_pull_for.modal.hide(); try { run_pull_for.repo.retrieveWork(run_pull_for.struct); updateRepoUser(w.id, run_pull_for.user.id); } catch(err) { ToastsStore.addToast({color: "danger", title: "Connexion impossible", msg: err}) };}}>
<div class="modal-header">
<h5 class="modal-title">Repository Pull {#if run_pull_for.user}{run_pull_for.user.login}{/if}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="form-group row mb-2">
<label class="col-2 col-form-label" for="pull-tag">Tag</label>
<input class="form-control col" id="pull-tag" autofocus placeholder={w.tag} bind:value={run_pull_for.struct.tag}>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1" id="optional-sig" bind:checked={run_pull_for.struct.sig_optional}>
<label class="form-check-label" for="optional-sig">
Signature du tag optionnelle
</label>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
Récupérer le travail
</button>
</div>
</form>
</div>
</div>
<div class="modal fade" tabindex="-1" id="repoModal">
<div class="modal-dialog">
<form class="modal-content" on:submit|preventDefault={() => {search_repo_for.modal.hide(); try { search_repo_for.repo.retrieveWork(run_pull_for.struct); updateRepoUser(w.id, run_pull_for.user.id); } catch(err) { ToastsStore.addToast({color: "danger", title: "Connexion impossible", msg: err}) }; search_repo_for.user = null; }}>
<div class="modal-header">
<h5 class="modal-title">Repository selector {#if search_repo_for.user}{search_repo_for.user.login}{/if}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{#if search_repo_for.user}
<WorkRepository work={w} user={search_repo_for.user} />
{/if}
</div>
</form>
</div>
</div>
<div class="modal fade" tabindex="-1" id="logsModal">
<div class="modal-dialog modal-xl">
<div class="modal-content">