ui: Working on works

This commit is contained in:
nemunaire 2022-07-08 11:53:50 +02:00
parent b9acaa798b
commit 197c23736d
12 changed files with 475 additions and 10 deletions

View File

@ -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

View File

@ -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(""))

View File

@ -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>

View 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>

View File

@ -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
View 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);
}
}

View File

@ -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>

View File

@ -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>

View 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 &hellip;</span>
</div>
{:then}
<slot></slot>
{:catch error}
<div class="text-center">
<h2>
<a href="works/" class="text-muted" style="text-decoration: none">&lt;</a>
Travail introuvable
</h2>
<span>{error}</span>
</div>
{/await}

View 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">&lt;</a>
{w.title}
</h2>
</div>
{#if $user && $user.is_admin}
<WorkAdmin work={w} on:saved={() => edit = false} />
{/if}
{/await}

View 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 &hellip;</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}

View 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">&lt;</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}