ui: Working on works
This commit is contained in:
parent
b9acaa798b
commit
197c23736d
@ -10,7 +10,7 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed ui/build/* ui/build/_app/* ui/build/_app/assets/pages/* ui/build/_app/pages/* ui/build/_app/pages/grades/* ui/build/_app/pages/surveys/* ui/build/_app/pages/surveys/_sid_/* ui/build/_app/pages/surveys/_sid_/responses/* ui/build/_app/pages/users/* ui/build/_app/pages/users/_uid_/* ui/build/_app/pages/users/_uid_/surveys/*
|
||||
//go:embed ui/build/* ui/build/_app/* ui/build/_app/assets/pages/* ui/build/_app/pages/* ui/build/_app/pages/grades/* ui/build/_app/pages/surveys/* ui/build/_app/pages/surveys/_sid_/* ui/build/_app/pages/surveys/_sid_/responses/* ui/build/_app/pages/users/* ui/build/_app/pages/users/_uid_/* ui/build/_app/pages/users/_uid_/surveys/* ui/build/_app/pages/works/_wid_/* ui/build/_app/pages/works/*
|
||||
var _assets embed.FS
|
||||
|
||||
var Assets http.FileSystem
|
||||
|
@ -59,6 +59,8 @@ func init() {
|
||||
Router().GET("/surveys/*_", serveOrReverse("/"))
|
||||
Router().GET("/users", serveOrReverse("/"))
|
||||
Router().GET("/users/*_", serveOrReverse("/"))
|
||||
Router().GET("/works", serveOrReverse("/"))
|
||||
Router().GET("/works/*_", serveOrReverse("/"))
|
||||
Router().GET("/css/*_", serveOrReverse(""))
|
||||
Router().GET("/fonts/*_", serveOrReverse(""))
|
||||
Router().GET("/img/*_", serveOrReverse(""))
|
||||
|
@ -7,7 +7,9 @@
|
||||
import { getSurveys } from '../lib/surveys';
|
||||
import { getScore } from '../lib/users';
|
||||
|
||||
let req_surveys = getSurveys();
|
||||
export let allworks = false;
|
||||
|
||||
let req_surveys = getSurveys(allworks);
|
||||
export let direct = null;
|
||||
|
||||
req_surveys.then((surveys) => {
|
||||
@ -17,6 +19,18 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function gotoSurvey(survey) {
|
||||
if (survey.kind === "w") {
|
||||
goto(`works/${survey.id}`);
|
||||
} else if (survey.direct != null) {
|
||||
goto(`surveys/${survey.id}/live`);
|
||||
} else if ($user.is_admin) {
|
||||
goto(`surveys/${survey.id}/responses`);
|
||||
} else {
|
||||
goto(`surveys/${survey.id}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
@ -38,7 +52,7 @@
|
||||
</tr>
|
||||
{:then surveys}
|
||||
<tbody style="cursor: pointer;">
|
||||
{#each surveys as survey, sid (survey.id)}
|
||||
{#each surveys as survey, sid (survey.kind + survey.id)}
|
||||
{#if (survey.shown || survey.direct != null || ($user && $user.is_admin)) && (!$user || (!$user.was_admin || $user.promo == survey.promo) || $user.is_admin)}
|
||||
{#if $user && $user.is_admin && (sid == 0 || surveys[sid-1].promo != survey.promo)}
|
||||
<tr class="bg-info text-light">
|
||||
@ -47,7 +61,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr on:click={e => goto(survey.direct != null ?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
|
||||
<tr on:click={e => gotoSurvey(survey)}>
|
||||
<td>
|
||||
{#if !survey.shown}<i class="bi bi-eye-slash-fill" title="Ce questionnaire n'est pas affiché aux étudiants"></i>{/if}
|
||||
{survey.title}
|
||||
@ -55,12 +69,12 @@
|
||||
<SurveyBadge {survey} class="float-end" />
|
||||
</td>
|
||||
{#if survey.startAvailability() > Date.now()}
|
||||
<td>
|
||||
<td title="Disponible à partir du {survey.start_availability}">
|
||||
<DateFormat date={survey.start_availability} dateStyle="medium" timeStyle="medium" />
|
||||
<i class="bi bi-arrow-bar-right"></i>
|
||||
</td>
|
||||
{:else}
|
||||
<td>
|
||||
<td title="Sera fermé le {survey.start_availability}">
|
||||
<i class="bi bi-arrow-bar-left"></i>
|
||||
<DateFormat date={survey.end_availability} dateStyle="medium" timeStyle="medium" />
|
||||
</td>
|
||||
|
135
ui/src/components/WorkAdmin.svelte
Normal file
135
ui/src/components/WorkAdmin.svelte
Normal file
@ -0,0 +1,135 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { ToastsStore } from '../stores/toasts';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let work = null;
|
||||
|
||||
function saveWork() {
|
||||
work.save().then((response) => {
|
||||
dispatch('saved', response);
|
||||
}, (error) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: error.errmsg,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function deleteWork() {
|
||||
work.delete().then((response) => {
|
||||
goto(`works`);
|
||||
}, (error) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: error.errmsg,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function duplicateWork() {
|
||||
work.duplicate().then((response) => {
|
||||
goto(`works/${response.id}`);
|
||||
}).catch((error) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: error.errmsg,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={saveWork}>
|
||||
|
||||
{#if work.id}
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="title" class="col-form-label col-form-label-sm">Identifiant du travail</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control-plaintext form-control-sm" id="title" value={work.id}>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="title" class="col-form-label col-form-label-sm">Titre du travail</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control form-control-sm" id="title" bind:value={work.title}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="promo" class="col-form-label col-form-label-sm">Promo</label>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-4 col-lg-2">
|
||||
<input type="number" step="1" min="0" max="2068" class="form-control form-control-sm" id="promo" bind:value={work.promo}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="group" class="col-form-label col-form-label-sm">Restreindre au groupe</label>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-4 col-lg-2">
|
||||
<input class="form-control form-control-sm" id="group" bind:value={work.group}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="submissionurl" class="col-form-label col-form-label-sm">URL validation la soumission</label>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-4 col-lg-2">
|
||||
<input class="form-control form-control-sm" id="submissionurl" bind:value={work.submission_url}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control form-control-sm" id="start_availability" bind:value={work.start_availability}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<input type="text" class="form-control form-control-sm" id="end_availability" bind:value={work.end_availability}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-3 mx-1 my-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="shown" bind:checked={work.shown}>
|
||||
<label class="form-check-label" for="shown">
|
||||
Afficher le travail
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="corrected" bind:checked={work.corrected}>
|
||||
<label class="form-check-label" for="corrected">
|
||||
Marqué comme corrigé
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
{#if work.id}
|
||||
<button type="button" class="btn btn-danger" on:click={deleteWork}>Supprimer</button>
|
||||
<button type="button" class="btn btn-secondary" on:click={duplicateWork}>Dupliquer avec ces nouveaux paramètres</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
@ -1,8 +1,10 @@
|
||||
import { getQuestions } from './questions';
|
||||
import { Response } from './response';
|
||||
import { Work } from './works';
|
||||
|
||||
export class Survey {
|
||||
constructor(res) {
|
||||
this.kind = "s";
|
||||
if (res) {
|
||||
this.update(res);
|
||||
}
|
||||
@ -126,10 +128,19 @@ export class Survey {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSurveys() {
|
||||
const res = await fetch(`api/surveys`, {headers: {'Accept': 'application/json'}})
|
||||
export async function getSurveys(allworks) {
|
||||
const res = await fetch(allworks?`api/all_works`:`api/surveys`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).map((s) => new Survey(s));
|
||||
if (allworks) {
|
||||
return (await res.json()).map((s) => {
|
||||
if (s.kind == "survey")
|
||||
return new Survey(s);
|
||||
else
|
||||
return new Work(s);
|
||||
});
|
||||
} else {
|
||||
return (await res.json()).map((s) => new Survey(s));
|
||||
}
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
|
108
ui/src/lib/works.js
Normal file
108
ui/src/lib/works.js
Normal file
@ -0,0 +1,108 @@
|
||||
export class Work {
|
||||
constructor(res) {
|
||||
this.kind = "w";
|
||||
if (res) {
|
||||
this.update(res);
|
||||
}
|
||||
}
|
||||
|
||||
update({ id, title, promo, group, shown, submission_url, corrected, start_availability, end_availability }) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.promo = promo;
|
||||
this.group = group;
|
||||
this.shown = shown;
|
||||
this.submission_url = submission_url;
|
||||
this.corrected = corrected;
|
||||
if (this.start_availability != start_availability) {
|
||||
this.start_availability = start_availability;
|
||||
delete this.__start_availability;
|
||||
}
|
||||
if (this.end_availability != end_availability) {
|
||||
this.end_availability = end_availability;
|
||||
delete this.__end_availability;
|
||||
}
|
||||
}
|
||||
|
||||
startAvailability() {
|
||||
if (!this.__start_availability) {
|
||||
this.__start_availability = new Date(this.start_availability)
|
||||
}
|
||||
return this.__start_availability
|
||||
}
|
||||
|
||||
endAvailability() {
|
||||
if (!this.__end_availability) {
|
||||
this.__end_availability = new Date(this.end_availability)
|
||||
}
|
||||
return this.__end_availability
|
||||
}
|
||||
|
||||
isFinished() {
|
||||
return this.endAvailability() < new Date();
|
||||
}
|
||||
|
||||
async save() {
|
||||
const res = await fetch(this.id?`api/works/${this.id}`:'api/works', {
|
||||
method: this.id?'PUT':'POST',
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
if (res.status == 200) {
|
||||
const data = await res.json()
|
||||
this.update(data);
|
||||
return data;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
async duplicate() {
|
||||
if (this.id) {
|
||||
const oldSurveyId = this.id;
|
||||
delete this.id;
|
||||
const res = await fetch(`api/works`, {
|
||||
method: 'POST',
|
||||
headers: {'Accept': 'application/json'},
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (this.id) {
|
||||
const res = await fetch(`api/works/${this.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {'Accept': 'application/json'},
|
||||
});
|
||||
if (res.status == 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWorks() {
|
||||
const res = await fetch(`api/works`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).map((s) => new Work(s));
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getWork(wid) {
|
||||
const res = await fetch(`api/works/${wid}`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return new Work(await res.json());
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
@ -89,6 +89,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{#if $user && $user.is_admin}
|
||||
<li class="nav-item"><a class="nav-link" class:active={rroute === 'works'} href="works">Travaux</a></li>
|
||||
<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>
|
||||
|
@ -67,7 +67,7 @@
|
||||
Vous devez <a href="auth/CRI" target="_self">vous identifier</a> pour accéder au contenu.
|
||||
</p>
|
||||
{/if}
|
||||
<SurveyList bind:direct={direct} />
|
||||
<SurveyList allworks bind:direct={direct} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
39
ui/src/routes/works/[wid]/__layout.svelte
Normal file
39
ui/src/routes/works/[wid]/__layout.svelte
Normal file
@ -0,0 +1,39 @@
|
||||
<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;
|
||||
</script>
|
||||
|
||||
{#await work}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement du rendu …</span>
|
||||
</div>
|
||||
{:then}
|
||||
<slot></slot>
|
||||
{:catch error}
|
||||
<div class="text-center">
|
||||
<h2>
|
||||
<a href="works/" class="text-muted" style="text-decoration: none"><</a>
|
||||
Travail introuvable
|
||||
</h2>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/await}
|
34
ui/src/routes/works/[wid]/index.svelte
Normal file
34
ui/src/routes/works/[wid]/index.svelte
Normal file
@ -0,0 +1,34 @@
|
||||
<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';
|
||||
|
||||
import { user } from '../../../stores/user';
|
||||
import SurveyBadge from '../../../components/SurveyBadge.svelte';
|
||||
import WorkAdmin from '../../../components/WorkAdmin.svelte';
|
||||
|
||||
export let work = null;
|
||||
</script>
|
||||
|
||||
{#await work then w}
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="works/" class="text-muted" style="text-decoration: none"><</a>
|
||||
{w.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<WorkAdmin work={w} on:saved={() => edit = false} />
|
||||
{/if}
|
||||
{/await}
|
99
ui/src/routes/works/index.svelte
Normal file
99
ui/src/routes/works/index.svelte
Normal file
@ -0,0 +1,99 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { user } from '../../stores/user';
|
||||
import DateFormat from '../../components/DateFormat.svelte';
|
||||
import SurveyBadge from '../../components/SurveyBadge.svelte';
|
||||
import { getWorks } from '../../lib/works';
|
||||
import { getPromos } from '../../lib/users';
|
||||
import { getScore } from '../../lib/users';
|
||||
|
||||
let filterPromo = "";
|
||||
</script>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<a href="works/new" class="btn btn-primary ml-1 float-end" title="Ajouter un travail">
|
||||
<i class="bi bi-plus"></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>
|
||||
Travaux
|
||||
</h2>
|
||||
|
||||
{#await getWorks()}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-danger mx-3" role="status"></div>
|
||||
<span>Chargement des travaux …</span>
|
||||
</div>
|
||||
{:then works}
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Intitulé</th>
|
||||
<th>Date</th>
|
||||
{#if $user}
|
||||
<th>Score</th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody style="cursor: pointer;">
|
||||
{#each works as work, wid (work.id)}
|
||||
{#if (work.shown || ($user && $user.is_admin)) && (!$user || (!$user.was_admin || $user.promo == work.promo) || $user.is_admin)}
|
||||
{#if $user && $user.is_admin && (wid == 0 || works[wid-1].promo != work.promo)}
|
||||
<tr class="bg-info text-light">
|
||||
<th colspan="5" class="fw-bold">
|
||||
{work.promo}
|
||||
</th>
|
||||
</tr>
|
||||
{/if}
|
||||
<tr on:click={e => goto(`works/${work.id}`)}>
|
||||
<td>
|
||||
{#if !work.shown}<i class="bi bi-eye-slash-fill" title="Ce travail n'est pas affiché aux étudiants"></i>{/if}
|
||||
{work.title}
|
||||
{#if work.group}<span class="badge bg-secondary">{work.group}</span>{/if}
|
||||
<SurveyBadge survey={work} class="float-end" />
|
||||
</td>
|
||||
{#if work.startAvailability() > Date.now()}
|
||||
<td title="Ce travail sera disponible à partir du {work.start_availability}">
|
||||
<DateFormat date={work.start_availability} dateStyle="medium" timeStyle="medium" />
|
||||
<i class="bi bi-arrow-bar-right"></i>
|
||||
</td>
|
||||
{:else}
|
||||
<td title="La date de rendu de ce travail est établie au {work.end_availability}">
|
||||
<i class="bi bi-arrow-bar-left"></i>
|
||||
<DateFormat date={work.end_availability} dateStyle="medium" timeStyle="medium" />
|
||||
</td>
|
||||
{/if}
|
||||
{#if $user}
|
||||
{#if !work.corrected}
|
||||
<td>N/A</td>
|
||||
{:else}
|
||||
<td>
|
||||
{#await getScore(work)}
|
||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||
{:then score}
|
||||
{score.score}
|
||||
{/await}
|
||||
</td>
|
||||
{/if}
|
||||
{/if}
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:catch error}
|
||||
<div class="text-center text-danger">
|
||||
{error.message}
|
||||
</div>
|
||||
{/await}
|
22
ui/src/routes/works/new.svelte
Normal file
22
ui/src/routes/works/new.svelte
Normal file
@ -0,0 +1,22 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { user } from '../../stores/user';
|
||||
import WorkAdmin from '../../components/WorkAdmin.svelte';
|
||||
import SurveyBadge from '../../components/SurveyBadge.svelte';
|
||||
import { Work } from '../../lib/works';
|
||||
|
||||
let work = new Work();
|
||||
</script>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="works/" class="text-muted" style="text-decoration: none"><</a>
|
||||
Nouveau travail
|
||||
</h2>
|
||||
<SurveyBadge class="ms-2" survey={work} />
|
||||
</div>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<WorkAdmin {work} on:saved={(e) => { goto(`works/${e.detail.id}`)}} />
|
||||
{/if}
|
Reference in New Issue
Block a user