Add categories to sort/filter works/surveys
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
b88d284859
commit
4c76dd9728
17 changed files with 586 additions and 25 deletions
60
ui/src/lib/categories.js
Normal file
60
ui/src/lib/categories.js
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
export async function getCategories() {
|
||||
let url = '/api/categories';
|
||||
const res = await fetch(url, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return (await res.json()).map((r) => new Category(r));
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
export class Category {
|
||||
constructor(res) {
|
||||
if (res) {
|
||||
this.update(res);
|
||||
}
|
||||
}
|
||||
|
||||
update({ id, label, promo, expand }) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.promo = promo;
|
||||
this.expand = expand;
|
||||
}
|
||||
|
||||
async save() {
|
||||
const res = await fetch(this.id?`api/categories/${this.id}`:'api/categories', {
|
||||
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 delete() {
|
||||
const res = await fetch(`api/categories/${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 getCategory(cid) {
|
||||
const res = await fetch(`api/categories/${cid}`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return new Category(await res.json());
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
92
ui/src/lib/components/CategoryAdmin.svelte
Normal file
92
ui/src/lib/components/CategoryAdmin.svelte
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import DateTimeInput from './DateTimeInput.svelte';
|
||||
import { ToastsStore } from '$lib/stores/toasts';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let category = null;
|
||||
|
||||
function saveCategory() {
|
||||
category.save().then((response) => {
|
||||
dispatch('saved', response);
|
||||
}, (error) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: error,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function deleteCategory() {
|
||||
category.delete().then((response) => {
|
||||
goto(`categories`);
|
||||
}, (error) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: error,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function duplicateCategory() {
|
||||
category.duplicate().then((response) => {
|
||||
goto(`categories/${response.id}`);
|
||||
}).catch((error) => {
|
||||
ToastsStore.addErrorToast({
|
||||
msg: error,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={saveCategory}>
|
||||
|
||||
{#if category.id}
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="cid" class="col-form-label col-form-label-sm">Identifiant de la catégorie</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control-plaintext form-control-sm" id="cid" value={category.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 de la catégorie</label>
|
||||
</div>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" class="form-control form-control-sm" id="title" bind:value={category.label}>
|
||||
</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={category.promo}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-3 mx-1 my-2">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="expand" bind:checked={category.expand}>
|
||||
<label class="form-check-label" for="expand">
|
||||
Étendre par défaut
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
||||
{#if category.id}
|
||||
<button type="button" class="btn btn-danger" on:click={deleteCategory}>Supprimer</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { getCategories } from '$lib/categories';
|
||||
import { getQuestions } from '$lib/questions';
|
||||
import DateTimeInput from './DateTimeInput.svelte';
|
||||
import { ToastsStore } from '$lib/stores/toasts';
|
||||
|
|
@ -67,6 +68,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="category" class="col-form-label col-form-label-sm">Catégorie</label>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-4 col-lg-2">
|
||||
{#await getCategories() then categories}
|
||||
<select id="category" class="form-select form-select-sm" bind:value={survey.id_category}>
|
||||
{#each categories as category (category.id)}
|
||||
<option value={category.id}>{category.label} {category.promo}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/await}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
|
||||
import SubmissionStatus from '$lib/components/SubmissionStatus.svelte';
|
||||
import { getCategories } from '$lib/categories';
|
||||
import { getSurveys } from '$lib/surveys';
|
||||
import { getScore } from '$lib/users';
|
||||
|
||||
|
|
@ -21,6 +22,13 @@
|
|||
}
|
||||
});
|
||||
|
||||
let categories = {};
|
||||
getCategories().then((cs) => {
|
||||
for (const c of cs) {
|
||||
categories[c.id] = c;
|
||||
}
|
||||
});
|
||||
|
||||
function gotoSurvey(survey) {
|
||||
if (survey.kind === "w") {
|
||||
goto(`works/${survey.id}`);
|
||||
|
|
@ -60,12 +68,30 @@
|
|||
{#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">
|
||||
<tr class="bg-warning text-light">
|
||||
<th colspan="5" class="fw-bold">
|
||||
{survey.promo}
|
||||
</th>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if $user && (sid == 0 || surveys[sid-1].id_category != survey.id_category) && categories[survey.id_category]}
|
||||
<tr class="bg-primary text-light">
|
||||
<th colspan="5" class="fw-bold" on:click={() => categories[survey.id_category].expand = !categories[survey.id_category].expand}>
|
||||
{#if categories[survey.id_category].expand}
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
{:else}
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
{/if}
|
||||
{categories[survey.id_category].label}
|
||||
{#if $user && $user.is_admin}
|
||||
<a href="categories/{survey.id_category}" class="float-end btn btn-sm btn-light" style="margin: -6px;">
|
||||
<i class="bi bi-pencil" on:click={() => categories[survey.id_category].expand = !categories[survey.id_category].expand}></i>
|
||||
</a>
|
||||
{/if}
|
||||
</th>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if categories[survey.id_category] && categories[survey.id_category].expand}
|
||||
<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}
|
||||
|
|
@ -127,6 +153,7 @@
|
|||
{/if}
|
||||
{/if}
|
||||
</tr>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { getCategories } from '$lib/categories';
|
||||
import DateTimeInput from './DateTimeInput.svelte';
|
||||
import { ToastsStore } from '$lib/stores/toasts';
|
||||
|
||||
|
|
@ -62,6 +63,21 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 text-sm-end">
|
||||
<label for="category" class="col-form-label col-form-label-sm">Catégorie</label>
|
||||
</div>
|
||||
<div class="col-sm-8 col-md-4 col-lg-2">
|
||||
{#await getCategories() then categories}
|
||||
<select id="category" class="form-select form-select-sm" bind:value={work.id_category}>
|
||||
{#each categories as category (category.id)}
|
||||
<option value={category.id}>{category.label} {category.promo}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/await}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ export class Survey {
|
|||
}
|
||||
}
|
||||
|
||||
update({ id, title, promo, group, shown, direct, corrected, start_availability, end_availability }) {
|
||||
update({ id, id_category, title, promo, group, shown, direct, corrected, start_availability, end_availability }) {
|
||||
this.id = id;
|
||||
this.id_category = id_category;
|
||||
this.title = title;
|
||||
this.promo = promo;
|
||||
this.group = group;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ export class Work {
|
|||
}
|
||||
}
|
||||
|
||||
update({ id, title, promo, group, shown, tag, description, descr_raw, submission_url, corrected, start_availability, end_availability }) {
|
||||
update({ id, id_category, title, promo, group, shown, tag, description, descr_raw, submission_url, corrected, start_availability, end_availability }) {
|
||||
this.id = id;
|
||||
this.id_category = id_category;
|
||||
this.title = title;
|
||||
this.promo = promo;
|
||||
this.group = group;
|
||||
|
|
|
|||
39
ui/src/routes/categories/[cid]/index.svelte
Normal file
39
ui/src/routes/categories/[cid]/index.svelte
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<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';
|
||||
|
||||
import { user } from '$lib/stores/user';
|
||||
import CategoryAdmin from '$lib/components/CategoryAdmin.svelte';
|
||||
import { Category, getCategory } from '$lib/categories';
|
||||
|
||||
export let cid;
|
||||
|
||||
let categoryP = null;
|
||||
$: {
|
||||
categoryP = getCategory(cid);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await categoryP then category}
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="categories/" class="text-muted" style="text-decoration: none"><</a>
|
||||
{category.label}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<CategoryAdmin {category} on:saved={(e) => { goto(`categories/`)}} />
|
||||
{/if}
|
||||
{/await}
|
||||
70
ui/src/routes/categories/index.svelte
Normal file
70
ui/src/routes/categories/index.svelte
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { user } from '$lib/stores/user';
|
||||
import { getCategories } from '$lib/categories';
|
||||
import { getPromos } from '$lib/users';
|
||||
|
||||
function showCategory(category) {
|
||||
goto(`categories/${category.id}`)
|
||||
}
|
||||
|
||||
let filterPromo = "";
|
||||
</script>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<a href="categories/new" class="btn btn-primary ml-1 float-end" title="Ajouter une catégorie">
|
||||
<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>
|
||||
Catégories // cours
|
||||
</h2>
|
||||
|
||||
{#await getCategories()}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-danger mx-3" role="status"></div>
|
||||
<span>Chargement des catégories …</span>
|
||||
</div>
|
||||
{:then categories}
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nom</th>
|
||||
<th>Promo</th>
|
||||
<th>Étendre</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each categories.filter((c) => (filterPromo === "" || filterPromo === c.promo)) as c (c.id)}
|
||||
<tr>
|
||||
<td>{c.id}</td>
|
||||
<td>
|
||||
<a href="categories/{c.id}">{c.label}</a>
|
||||
</td>
|
||||
<td>{c.promo}</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
class:bg-success={c.expand}
|
||||
class:bg-danger={!c.expand}
|
||||
>
|
||||
{c.expand?"Oui":"Non"}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/await}
|
||||
20
ui/src/routes/categories/new.svelte
Normal file
20
ui/src/routes/categories/new.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { user } from '$lib/stores/user';
|
||||
import CategoryAdmin from '$lib/components/CategoryAdmin.svelte';
|
||||
import { Category } from '$lib/categories';
|
||||
|
||||
let category = new Category();
|
||||
</script>
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="categories/" class="text-muted" style="text-decoration: none"><</a>
|
||||
Nouvelle catégorie
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{#if $user && $user.is_admin}
|
||||
<CategoryAdmin {category} on:saved={(e) => { goto(`categories/${e.detail.id}`)}} />
|
||||
{/if}
|
||||
|
|
@ -5,17 +5,28 @@
|
|||
import DateFormat from '$lib/components/DateFormat.svelte';
|
||||
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
|
||||
import SubmissionStatus from '$lib/components/SubmissionStatus.svelte';
|
||||
import { getCategories } from '$lib/categories';
|
||||
import { getWorks } from '$lib/works';
|
||||
import { getPromos } from '$lib/users';
|
||||
import { getScore } from '$lib/users';
|
||||
|
||||
let filterPromo = "";
|
||||
|
||||
let categories = {};
|
||||
getCategories().then((cs) => {
|
||||
for (const c of cs) {
|
||||
categories[c.id] = c;
|
||||
}
|
||||
});
|
||||
</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>
|
||||
<a href="categories/" class="btn btn-info mx-1 float-end" title="Modifier les catégories">
|
||||
<i class="bi bi-filter-circle"></i>
|
||||
</a>
|
||||
{#await getPromos() then promos}
|
||||
<div class="float-end me-2">
|
||||
<select class="form-select" bind:value={filterPromo}>
|
||||
|
|
@ -51,12 +62,30 @@
|
|||
{#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">
|
||||
<tr class="bg-warning text-light">
|
||||
<th colspan="5" class="fw-bold">
|
||||
{work.promo}
|
||||
</th>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if $user && (wid == 0 || works[wid-1].id_category != work.id_category) && categories[work.id_category]}
|
||||
<tr class="bg-primary text-light">
|
||||
<th colspan="5" class="fw-bold" on:click={() => categories[work.id_category].expand = !categories[work.id_category].expand}>
|
||||
{#if categories[work.id_category].expand}
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
{:else}
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
{/if}
|
||||
{categories[work.id_category].label}
|
||||
{#if $user && $user.is_admin}
|
||||
<a href="categories/{work.id_category}" class="float-end btn btn-sm btn-light" style="margin: -6px;">
|
||||
<i class="bi bi-pencil" on:click={() => categories[work.id_category].expand = !categories[work.id_category].expand}></i>
|
||||
</a>
|
||||
{/if}
|
||||
</th>
|
||||
</tr>
|
||||
{/if}
|
||||
{#if categories[work.id_category] && categories[work.id_category].expand}
|
||||
<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}
|
||||
|
|
@ -94,6 +123,7 @@
|
|||
{/if}
|
||||
{/if}
|
||||
</tr>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
|
|
|
|||
Reference in a new issue