From faa6f552d40137b254a6e9cd9166b2edc0a7247a Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 11 Jun 2024 08:58:05 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Assiduit=C3=A9=20:=20suppression=20signal?= =?UTF-8?q?=5Fassiduites=5Fdiff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_moduleimpl_status.py | 14 - .../pages/signal_assiduites_diff.j2 | 702 ------------------ app/views/assiduites.py | 111 --- 3 files changed, 827 deletions(-) delete mode 100644 app/templates/assiduites/pages/signal_assiduites_diff.j2 diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 0588cc98..07c3266e 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -370,20 +370,6 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): >Saisie Absences (Hebdo) """ ) - H.append( - f""" - (Saisie Absences Différée) - """ - ) - H.append("") # if not modimpl.check_apc_conformity(nt): diff --git a/app/templates/assiduites/pages/signal_assiduites_diff.j2 b/app/templates/assiduites/pages/signal_assiduites_diff.j2 deleted file mode 100644 index eb85cb5c..00000000 --- a/app/templates/assiduites/pages/signal_assiduites_diff.j2 +++ /dev/null @@ -1,702 +0,0 @@ -{% extends "sco_page.j2" %} - -{% block styles %} -{{ super() }} - - - - - -{% endblock styles %} - -{% block scripts %} -{{ super() }} - - - -{% include "sco_timepicker.j2" %} - - - -{% endblock scripts %} - -{% block title %} -{{title}} -{% endblock title %} - -{% block app_content %} - -

Signalement différé de l'assiduité {{gr |safe}}

- -
-Attention, cette page va prochainement être supprimée, car il est plus facile d'utiliser - -

Ci-dessous le formulaire vous permettant de saisir plusieurs plages à la fois, -qui va bientôt être retiré. -

-

N'hésitez pas à commenter sur le salon Discord -si vous avez d'autres besoins. -

-
- - - -
- - -
- - - - - - - -
-
- -
-
- - - - - -
- - - - - - -{% include "assiduites/widgets/alert.j2" %} -{% endblock app_content %} diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 409bb43a..105222b9 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1724,117 +1724,6 @@ def _preparer_objet( return objet_prepare -@bp.route("/signal_assiduites_diff") -@scodoc -@permission_required(Permission.AbsChange) -def signal_assiduites_diff(): - """ - Utilisé notamment par "Saisie différée" sur tableau de bord semetstre" - - Arguments de la requête: - - - group_ids : liste des groupes - example : group_ids=1,2,3 - - formsemestre_id : id du formsemestre - example : formsemestre_id=1 - - moduleimpl_id : id du moduleimpl - example : moduleimpl_id=1 - - (Permet de pré-générer une plage. Si non renseigné, la plage sera vide) - (Les trois valeurs suivantes doivent être renseignées ensemble) - - date - example : date=01/01/2021 - - heure_debut - example : heure_debut=08:00 - - heure_fin - example : heure_fin=10:00 - - Exemple de requête : - signal_assiduites_diff?formsemestre_id=67&group_ids=400&moduleimpl_id=1229&date=15/04/2024&heure_debut=12:34&heure_fin=12:55 - """ - # Récupération des paramètres de la requête - group_ids: list[int] = request.args.get("group_ids", None) - formsemestre_id: int = request.args.get("formsemestre_id", -1) - formsemestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id) - - etudiants: list[Identite] = [] - - # Vérification des groupes - if group_ids is None: - group_ids = [] - else: - group_ids = group_ids.split(",") - map(str, group_ids) - groups_infos = sco_groups_view.DisplayedGroupsInfos( - group_ids, formsemestre_id=formsemestre.id, select_all_when_unspecified=True - ) - if not groups_infos.members: - return ( - html_sco_header.sco_header(page_title="Assiduité: saisie différée") - + "

Aucun étudiant !

" - + html_sco_header.sco_footer() - ) - - # Récupération des étudiants - etudiants.extend( - [Identite.get_etud(etudid=m["etudid"]) for m in groups_infos.members] - ) - etudiants = list(sorted(etudiants, key=lambda etud: etud.sort_key)) - - if groups_infos.tous_les_etuds_du_sem: - gr_tit = "en" - else: - if len(groups_infos.group_ids) > 1: - grp = "des groupes" - else: - grp = "du groupe" - gr_tit = ( - grp + ' ' + groups_infos.groups_titles + "" - ) - - # Pré-remplissage des sélecteurs - moduleimpl_id = request.args.get("moduleimpl_id", -1) - try: - moduleimpl_id = int(moduleimpl_id) - except ValueError: - moduleimpl_id = -1 - # date fra (dd/mm/yyyy) - date = request.args.get("date", "") - # heures (hh:mm) - heure_deb = request.args.get("heure_debut", "") - heure_fin = request.args.get("heure_fin", "") - - # vérifications des sélecteurs - date = date if re.match(r"^\d{2}\/\d{2}\/\d{4}$", date) else "" - heure_deb = heure_deb if re.match(r"^[0-2]\d:[0-5]\d$", heure_deb) else "" - heure_fin = heure_fin if re.match(r"^[0-2]\d:[0-5]\d$", heure_fin) else "" - nouv_plage: list[str] = [date, heure_deb, heure_fin] - - return render_template( - "assiduites/pages/signal_assiduites_diff.j2", - etudiants=etudiants, - moduleimpl_select=_module_selector( - formsemestre=formsemestre, moduleimpl_id=moduleimpl_id - ), - gr=gr_tit, - nonworkdays=_non_work_days(), - sco=ScoData(formsemestre=formsemestre), - forcer_module=sco_preferences.get_preference( - "forcer_module", - formsemestre_id=formsemestre_id, - dept_id=g.scodoc_dept_id, - ), - non_present=sco_preferences.get_preference( - "non_present", - formsemestre_id=formsemestre_id, - dept_id=g.scodoc_dept_id, - ), - nouv_plage=nouv_plage, - formsemestre_id=formsemestre_id, - group_ids=group_ids, - ) - - @bp.route("/signale_evaluation_abs//") @scodoc @permission_required(Permission.AbsChange) From 70605edad7e0dd790314cd5baa6db2f7d946737b Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 11 Jun 2024 14:50:30 +0200 Subject: [PATCH 2/3] =?UTF-8?q?Assiduit=C3=A9=20:=20documentation=20code?= =?UTF-8?q?=20JS=20+=20cleanup=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/assiduites.js | 107 +++++++++++-- app/templates/assiduites/pages/bilan_etud.j2 | 48 ++++-- .../assiduites/pages/calendrier_assi_etud.j2 | 24 ++- .../assiduites/pages/visu_assi_group.j2 | 7 +- app/templates/assiduites/widgets/alert.j2 | 16 +- .../assiduites/widgets/minitimeline.j2 | 8 +- app/templates/assiduites/widgets/prompt.j2 | 71 ++++++--- app/templates/assiduites/widgets/tableau.j2 | 6 +- .../widgets/tableau_actions/details.j2 | 140 ------------------ .../widgets/tableau_actions/modifier.j2 | 117 --------------- app/templates/assiduites/widgets/timeline.j2 | 138 +++++++++++------ app/templates/assiduites/widgets/toast.j2 | 52 ++++--- app/views/assiduites.py | 114 +------------- 13 files changed, 352 insertions(+), 496 deletions(-) delete mode 100644 app/templates/assiduites/widgets/tableau_actions/details.j2 delete mode 100644 app/templates/assiduites/widgets/tableau_actions/modifier.j2 diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index 0d12635c..4bcb8396 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -168,6 +168,10 @@ function creerLigneEtudiant(etud, index) { date_fin: null, }; + /**Retourne une liste d'assiduité en conflit avec la période actuelle + * @param {Array} assiduites - Les assiduités de l'étudiant + * @returns {Array} Les assiduités en conflit + */ function recupConflitsAssiduites(assiduites) { const period = getPeriodAsDate(); @@ -182,9 +186,12 @@ function creerLigneEtudiant(etud, index) { ); }); } - + // Pas de conflit en readonly const conflits = readOnly ? [] : recupConflitsAssiduites(etud.assiduites); + // Si il y a des conflits, on prend le premier pour l'afficher + // si les dates de début et de fin sont les mêmes, c'est une édition + // sinon c'est un conflit if (conflits.length > 0) { currentAssiduite = conflits[0]; @@ -200,6 +207,49 @@ function creerLigneEtudiant(etud, index) { : "conflit"; } + // Création de la ligne étudiante en DOM + /* exemple de ligne étudiante +
+
1
+ +
+
+
13h +
+
+
+
+ + + +
+
+ */ const ligneEtud = document.createElement("div"); ligneEtud.classList.add("etud_row"); if (Object.keys(etudsDefDem).includes(etud.id)) { @@ -388,6 +438,9 @@ async function creerTousLesEtudiants(etuds) { etudsDiv.innerHTML = ""; const moduleImplId = readOnly ? null : $("#moduleimpl_select").val(); const inscriptions = await getInscriptionModule(moduleImplId); + // on trie les étudiants par ordre alphabétique + // et on garde ceux qui sont inscrits au module + // puis pour chaque étudiant on crée une ligne [...etuds.values()] .sort((a, b) => { return a.sort_key > b.sort_key ? 1 : -1; @@ -496,10 +549,9 @@ async function getInscriptionModule(moduleimpl_id) { return inscriptionsModules.get(moduleimpl_id); } - +// Mise à jour de la ligne étudiant async function MiseAJourLigneEtud(etud) { //Récupérer ses assiduités - function RecupAssiduitesEtudiant(etudid) { const date = $("#date").datepicker("getDate"); const date_debut = date.add(-1, "days").format("YYYY-MM-DDTHH:mm"); @@ -527,6 +579,8 @@ async function MiseAJourLigneEtud(etud) { } await RecupAssiduitesEtudiant(etud.id); + // Une fois les assiduités récupérées, on met à jour la ligne étudiant + // on replace l'ancienne ligne par la nouvellement générée const etudRow = document.getElementById(`etud_row_${etud.id}`); if (etudRow == null) return; @@ -540,12 +594,14 @@ async function MiseAJourLigneEtud(etud) { etudRow.replaceWith(ligneEtud); } +// Action appelée lors d'un clic sur un bouton d'assiduité +// Création, édition ou suppression d'une assiduité async function actionAssiduite(etud, etat, type, assiduite = null) { const modimpl_id = $("#moduleimpl_select").val(); if (assiduite && assiduite.etat.toLowerCase() === etat) type = "suppression"; const { deb, fin } = getPeriodAsDate(); - + // génération d'un objet assiduité basique qui sera complété let assiduiteObjet = assiduite ?? { date_debut: deb, date_fin: fin, @@ -554,7 +610,8 @@ async function actionAssiduite(etud, etat, type, assiduite = null) { assiduiteObjet.etat = etat; assiduiteObjet.moduleimpl_id = modimpl_id; - + // En fonction du type d'action on appelle la bonne route + // avec les bonnes valeurs if (type === "creation") { await async_post( `../../api/assiduite/${etud.id}/create`, @@ -606,7 +663,9 @@ async function actionAssiduite(etud, etat, type, assiduite = null) { ); } } - +// Fonction pour afficher un message d'erreur si le module n'est pas renseigné +// ou si l'étudiant n'est pas inscrit au module. +// On donne le message d'erreur d'une requête api et cela affiche le message correspondant function erreurModuleImpl(message) { if (message == "Module non renseigné") { const HTML = ` @@ -635,7 +694,9 @@ function erreurModuleImpl(message) { openAlertModal("Sélection du module", content); } } - +// Fonction pour ajouter en lot une assiduité à tous les étudiants +// Fonctionne uniquement pour créer ou supprimer des assiduités +// Pas d'édition possible function mettreToutLeMonde(etat, el = null) { const lignesEtuds = [...document.querySelectorAll("fieldset.btns_field")]; @@ -709,7 +770,7 @@ function mettreToutLeMonde(etat, el = null) { } envoiToastTous("remove", assiduites_id.length); if (Object.keys(unDeleted).length == 0) return; - + // CAS : des assiduités d'étudiants n'ont pas pu être supprimés let unDeletedEtuds = `
    ${Object.keys(unDeleted) @@ -771,6 +832,7 @@ function mettreToutLeMonde(etat, el = null) { }); } +// Affichage d'un loader (animation jeu pong) function afficheLoader() { const loaderDiv = document.createElement("div"); loaderDiv.id = "loader"; @@ -782,11 +844,13 @@ function afficheLoader() { loaderDiv.appendChild(loader); document.body.appendChild(loaderDiv); } - +// Retrait du loader (animation jeu pong) function retirerLoader() { document.getElementById("loader").remove(); } +// Simplification de l'envoie de toast pour un étudiant +// affiche le nom, le prénom et l'état de l'assiduité avec une couleur spécifique function envoiToastEtudiant(etat, etud) { let etatAffiche; @@ -810,8 +874,10 @@ function envoiToastEtudiant(etat, etud) { pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); } - -// TODO commenter toutes les fonctions js +// Fonction pour simplifier l'envoie de toast avec le bouton "mettre tout le monde" +// On donne un etat et un compte et cela affichera le message associé. +// ex : 12 assiduités ont été supprimées +// ex : 15 étudiants ont été mis Absent. function envoiToastTous(etat, count) { const span = document.createElement("span"); let etatAffiche = etat; @@ -840,7 +906,9 @@ function envoiToastTous(etat, count) { pushToast(generateToast(span, getToastColorFromEtat(etat.toUpperCase()), 5)); } - +// Permet de savoir si un jour est travaillé ou pas +// jour : Date +// nonWorkdays : Array[str] => ["mar", "sam", "dim"] function estJourTravail(jour, nonWorkdays) { const d = Intl.DateTimeFormat("fr-FR", { timeZone: SCO_TIMEZONE, @@ -851,6 +919,9 @@ function estJourTravail(jour, nonWorkdays) { return !nonWorkdays.includes(d); } +// Renvoie le dernier jour travaillé disponible. +// par défaut va en arrière (dans le passé) +// si anti == False => va dans le futur function retourJourTravail(date, anti = true) { const jourMiliSecondes = 86400000; // 24 * 3600 * 1000 | H * s * ms let jour = date; @@ -864,12 +935,18 @@ function retourJourTravail(date, anti = true) { } return jour; } - +// Vérifie si la date courante est travaillée +// Si ce n'est pas le cas, on change la date pour le dernier jour travaillé (passé) +// et on affiche une alerte +// (utilise le datepicker #date) function dateCouranteEstTravaillee() { const date = $("#date").datepicker("getDate"); + if (!estJourTravail(date, nonWorkDays)) { + // récupération du jour travaillé le plus proche const nouvelleDate = retourJourTravail(date); $("#date").datepicker("setDate", nouvelleDate); + // Création du message d'alerte let msg = "Le jour sélectionné"; if (new Date().format("YYYY-MM-DD") == date.format("YYYY-MM-DD")) { msg = "Aujourd'hui"; @@ -889,13 +966,15 @@ function dateCouranteEstTravaillee() { )}.` ) ); + // Affichage de l'alerte openAlertModal("Attention", div, "", "#eec660"); return false; } return true; } - +// Fonction pour passer au jour suivant +// anti : bool => si true, on va dans le passé function jourSuivant(anti = false) { let date = $("#date").datepicker("getDate"); diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 6bb97053..74586c5f 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -111,7 +111,13 @@ Bilan assiduité de {{sco.etud.nomprenom}} {% endblock promptModal %} \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau.j2 b/app/templates/assiduites/widgets/tableau.j2 index 1517ebcb..a392b7f8 100644 --- a/app/templates/assiduites/widgets/tableau.j2 +++ b/app/templates/assiduites/widgets/tableau.j2 @@ -154,7 +154,7 @@ - diff --git a/app/templates/assiduites/widgets/timeline.j2 b/app/templates/assiduites/widgets/timeline.j2 index d7acdbe8..eaac2b84 100644 --- a/app/templates/assiduites/widgets/timeline.j2 +++ b/app/templates/assiduites/widgets/timeline.j2 @@ -27,21 +27,25 @@ let handleMoving = false; + // Création des graduations de la timeline + // On créé des grandes graduations pour les heures + // On créé des petites graduations pour les "tick" function createTicks() { let i = t_start; while (i <= t_end) { + // création d'un tick Heure (grand) const hourTick = document.createElement("div"); hourTick.classList.add("tick", "hour"); hourTick.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; timelineContainer.appendChild(hourTick); - + // on ajoute un label pour l'heure (ex : 12:00) const tickLabel = document.createElement("div"); tickLabel.classList.add("tick-label"); tickLabel.style.left = `${((i - t_start) / (t_end - t_start)) * 100}%`; tickLabel.textContent = numberToTime(i); timelineContainer.appendChild(tickLabel); - + // Si on est pas à la fin, on ajoute les graduations intermédiaires if (i < t_end) { let j = Math.floor(i + 1); @@ -49,6 +53,7 @@ i += tick_delay; if (i <= t_end) { + // création d'un tick (petit) const quarterTick = document.createElement("div"); quarterTick.classList.add("tick", "quarter"); quarterTick.style.left = `${computePercentage(i, t_start)}%`; @@ -62,7 +67,8 @@ } } } - + // Convertit un nombre en heure + // ex : 12.5 => "12:30" function numberToTime(num) { const integer = Math.floor(num); const decimal = Math.round((num % 1) * 60); @@ -80,13 +86,12 @@ return int + dec; } - + // Arrondi un nombre au tick le plus proche function snapToQuarter(value) { - - return Math.round(value * tick_time) / tick_time; } - + // Mise à jour des valeurs des timepickers + // En fonction des valeurs de la timeline function updatePeriodTimeLabel() { const values = getPeriodValues(); const deb = numberToTime(values[0]) @@ -102,96 +107,112 @@ } + // Gestion des évènements de la timeline + // - Déplacement des poignées + // - Déplacement de la période function timelineMainEvent(event) { + // Position de départ de l'événement (souris ou tactile) const startX = (event.clientX || event.changedTouches[0].clientX); + // Vérifie si l'événement concerne une poignée de période if (event.target.classList.contains("period-handle")) { + // Initialisation des valeurs de départ const startWidth = parseFloat(periodTimeLine.style.width); const startLeft = parseFloat(periodTimeLine.style.left); const isLeftHandle = event.target.classList.contains("left"); - handleMoving = true + handleMoving = true; + + // Fonction de déplacement de la poignée const onMouseMove = (moveEvent) => { if (!handleMoving) return; + // Calcul du déplacement en pixels const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; const containerWidth = timelineContainer.clientWidth; - const newWidth = - startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; + // Calcul de la nouvelle largeur en pourcentage + const newWidth = startWidth + ((isLeftHandle ? -deltaX : deltaX) / containerWidth) * 100; if (isLeftHandle) { + // Si la poignée gauche est déplacée, ajuste également la position gauche const newLeft = startLeft + (deltaX / containerWidth) * 100; adjustPeriodPosition(newLeft, newWidth); } else { adjustPeriodPosition(parseFloat(periodTimeLine.style.left), newWidth); } + // Met à jour l'étiquette de temps de la période updatePeriodTimeLabel(); }; + + // Fonction de relâchement de la souris ou du tactile + // - Alignement des poignées sur les ticks + // - Appel des callbacks + // - Sauvegarde des valeurs dans le local storage + // - Réinitialisation de la variable de déplacement des poignées const mouseUp = () => { snapHandlesToQuarters(); timelineContainer.removeEventListener("mousemove", onMouseMove); handleMoving = false; func_call(); savePeriodInLocalStorage(); + }; - } + // Ajoute les écouteurs d'événement pour le déplacement et le relâchement timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove); - document.addEventListener( - "mouseup", - mouseUp, - { once: true } - ); - document.addEventListener( - "touchend", - mouseUp, - { once: true } + document.addEventListener("mouseup", mouseUp, { once: true }); + document.addEventListener("touchend", mouseUp, { once: true }); - ); + // Vérifie si l'événement concerne la période elle-même } else if (event.target === periodTimeLine) { const startLeft = parseFloat(periodTimeLine.style.left); + // Fonction de déplacement de la période const onMouseMove = (moveEvent) => { if (handleMoving) return; const deltaX = (moveEvent.clientX || moveEvent.changedTouches[0].clientX) - startX; const containerWidth = timelineContainer.clientWidth; + // Calcul de la nouvelle position gauche en pourcentage const newLeft = startLeft + (deltaX / containerWidth) * 100; adjustPeriodPosition(newLeft, parseFloat(periodTimeLine.style.width)); - updatePeriodTimeLabel(); }; + + // Fonction de relâchement de la souris ou du tactile + // - Alignement des poignées sur les ticks + // - Appel des callbacks + // - Sauvegarde des valeurs dans le local storage const mouseUp = () => { snapHandlesToQuarters(); timelineContainer.removeEventListener("mousemove", onMouseMove); func_call(); savePeriodInLocalStorage(); - } + }; + + // Ajoute les écouteurs d'événement pour le déplacement et le relâchement timelineContainer.addEventListener("mousemove", onMouseMove); timelineContainer.addEventListener("touchmove", onMouseMove); - document.addEventListener( - "mouseup", - mouseUp, - { once: true } - ); - document.addEventListener( - "touchend", - mouseUp, - { once: true } - ); + document.addEventListener("mouseup", mouseUp, { once: true }); + document.addEventListener("touchend", mouseUp, { once: true }); } } + let func_call = () => { }; + // Fonction initialisant la timeline + // La fonction "callback" est appelée à chaque modification de la période function setupTimeLine(callback) { func_call = callback; timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) }); timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) }); + // Initialisation des timepickers (à gauche de la timeline) + // lors d'un changement, cela met à jour la timeline const updateFromInputs = ()=>{ let deb = $('#deb').val(); let fin = $('#fin').val(); @@ -209,9 +230,11 @@ $('#deb').data('TimePicker').options.change = updateFromInputs; $('#fin').data('TimePicker').options.change = updateFromInputs; + // actualise l'affichage des inputs avec les valeurs de la timeline updatePeriodTimeLabel(); } - + // Ajuste la position de la période en fonction de la nouvelle position et largeur + // Vérifie que la période ne dépasse pas les limites de la timeline function adjustPeriodPosition(newLeft, newWidth) { const snappedLeft = snapToQuarter(newLeft); @@ -224,30 +247,36 @@ periodTimeLine.style.left = `${clampedLeft}%`; periodTimeLine.style.width = `${snappedWidth}%`; } - + // Récupère les valeurs de la période function getPeriodValues() { + // On prend les pourcentages const leftPercentage = parseFloat(periodTimeLine.style.left); const widthPercentage = parseFloat(periodTimeLine.style.width); + // On calcule l'inverse des pourcentages pour obtenir les heures const startHour = (leftPercentage / 100) * (t_end - t_start) + t_start; const endHour = ((leftPercentage + widthPercentage) / 100) * (t_end - t_start) + t_start; - + // On les arrondit aux ticks les plus proches const startValue = snapToQuarter(startHour); const endValue = snapToQuarter(endHour); - + + // on verifie que les valeurs sont bien dans les bornes const computedValues = [Math.max(startValue, t_start), Math.min(t_end, endValue)]; - + + // si les valeurs sont hors des bornes, on les ajuste if (computedValues[0] > t_end || computedValues[1] < t_start) { return [t_start, Math.min(t_end, t_start + period_default)]; } - + // Si la période est trop petite, on l'agrandit artificiellement (il faut au moins 1 tick de largeur) if (computedValues[1] - computedValues[0] <= tick_delay && computedValues[1] < t_end - tick_delay) { computedValues[1] += tick_delay; } return computedValues; } - + // Met à jour les valeurs de la période + // Met à jour l'affichage de la timeline + // Appelle les callbacks associés function setPeriodValues(deb, fin) { if (fin < deb) { throw new RangeError(`le paramètre 'deb' doit être inférieur au paramètre 'fin' ([${deb};${fin}])`) @@ -259,8 +288,8 @@ deb = snapToQuarter(deb); fin = snapToQuarter(fin); - let leftPercentage = (deb - t_start) / (t_end - t_start) * 100; - let widthPercentage = (fin - deb) / (t_end - t_start) * 100; + let leftPercentage = computePercentage(deb, t_start); + let widthPercentage = computePercentage(fin, deb); periodTimeLine.style.left = `${leftPercentage}%`; periodTimeLine.style.width = `${widthPercentage}%`; @@ -269,7 +298,9 @@ func_call(); savePeriodInLocalStorage(); } - + // Aligne les poignées de la période sur les ticks les plus proches + // ex : 12h39 => 12h45 (si les ticks sont à 15min) + // evite aussi les dépassements de la timeline (max et min) function snapHandlesToQuarters() { const periodValues = getPeriodValues(); let lef = Math.min(computePercentage(Math.abs(periodValues[0]), t_start), computePercentage(Math.abs(t_end), tick_delay)); @@ -288,15 +319,20 @@ updatePeriodTimeLabel() } - + // Retourne le pourcentage d'une valeur par rapport à t_start et t_end + // ex : 12h par rapport à 8h et 20h => 25% function computePercentage(a, b) { return ((a - b) / (t_end - t_start)) * 100; } + // Convertit une heure (string) en nombre + // ex : "12:30" => 12.5 function fromTime(time, separator = ":") { const [hours, minutes] = time.split(separator).map((el) => Number(el)) return hours + minutes / 60 } - + // Renvoie les valeurs de la période sous forme de date + // Les heures sont récupérées depuis la timeline + // la date est récupérée depuis un champ "#date" (datepicker) function getPeriodAsDate(){ let [deb, fin] = getPeriodValues(); deb = numberToTime(deb); @@ -305,19 +341,21 @@ const dateStr = $("#date") .datepicker("getDate") .format("yyyy-mm-dd") - .substring(0, 10); + .substring(0, 10); // récupération que de la date, pas des heures return { deb: new Date(`${dateStr}T${deb}`), fin: new Date(`${dateStr}T${fin}`) } } - + // Sauvegarde les valeurs de la période dans le local storage function savePeriodInLocalStorage(){ const dates = getPeriodValues(); localStorage.setItem("sco-timeline-values", JSON.stringify(dates)); } + // Récupère les valeurs de la période depuis le local storage + // Si elles n'existent pas, on les initialise avec les valeurs par défaut function loadPeriodFromLocalStorage(){ const dates = JSON.parse(localStorage.getItem("sco-timeline-values")); if(dates){ @@ -326,11 +364,13 @@ setPeriodValues(t_start, t_start + period_default); } } + // == Initialisation par défaut de la timeline == - createTicks(); + createTicks(); // création des graduations - loadPeriodFromLocalStorage(); + loadPeriodFromLocalStorage(); // chargement des valeurs si disponible + // Si on donne les heures en appelant le template alors on met à jour la timeline {% if heures %} let [heure_deb, heure_fin] = [{{ heures | safe }}] if (heure_deb != '' && heure_fin != '') { diff --git a/app/templates/assiduites/widgets/toast.j2 b/app/templates/assiduites/widgets/toast.j2 index 6081d227..b75cc1e4 100644 --- a/app/templates/assiduites/widgets/toast.j2 +++ b/app/templates/assiduites/widgets/toast.j2 @@ -68,32 +68,49 @@ \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 105222b9..00c4d317 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1537,116 +1537,10 @@ def tableau_assiduite_actions(): flash(f"{objet_name} justifiée") return redirect(request.referrer) - if request.method == "GET": - module: str | int = "" # moduleimpl_id ou chaine libre - - if obj_type == "assiduite": - # Construction du menu module - module = _module_selector_multiple(objet.etudiant, objet.moduleimpl_id) - - return render_template( - "assiduites/pages/tableau_assiduite_actions.j2", - action=action, - can_view_justif_detail=current_user.has_permission(Permission.AbsJustifView) - or (obj_type == "justificatif" and current_user.id == objet.user_id), - etud=objet.etudiant, - moduleimpl=module, - obj_id=obj_id, - objet_name=objet_name, - objet=_preparer_objet(obj_type, objet), - sco=ScoData(etud=objet.etudiant), - title=f"Assiduité {objet.etudiant.nom_short}", - # type utilisé dans les actions modifier / détails (modifier.j2, details.j2) - type="Justificatif" if obj_type == "justificatif" else "Assiduité", - ) - # ----- Cas POST - if obj_type == "assiduite": - try: - _action_modifier_assiduite(objet) - except ScoValueError as error: - raise ScoValueError(error.args[0], request.referrer) from error - flash("L'assiduité a bien été modifiée.") - else: - try: - _action_modifier_justificatif(objet) - except ScoValueError as error: - raise ScoValueError(error.args[0], request.referrer) from error - flash("Le justificatif a bien été modifié.") - return redirect(request.form["table_url"]) - - -def _action_modifier_assiduite(assi: Assiduite): - form = request.form - - # Gestion de l'état - etat = scu.EtatAssiduite.get(form["etat"]) - if etat is not None: - assi.etat = etat - if etat == scu.EtatAssiduite.PRESENT: - assi.est_just = False - else: - assi.est_just = len(get_assiduites_justif(assi.assiduite_id, False)) > 0 - - # Gestion de la description - assi.description = form["description"] - - possible_moduleimpl_id: str = form["moduleimpl_select"] - - # Raise ScoValueError (si None et force module | Etudiant non inscrit | Module non reconnu) - assi.set_moduleimpl(possible_moduleimpl_id) - - db.session.add(assi) - db.session.commit() - scass.simple_invalidate_cache(assi.to_dict(True), assi.etudid) - - -def _action_modifier_justificatif(justi: Justificatif): - "Modifie le justificatif avec les valeurs dans le form" - form = request.form - - # Gestion des Dates - date_debut: datetime = scu.is_iso_formated(form["date_debut"], True) - date_fin: datetime = scu.is_iso_formated(form["date_fin"], True) - if date_debut is None or date_fin is None or date_fin < date_debut: - raise ScoValueError("Dates invalides", request.referrer) - justi.date_debut = date_debut - justi.date_fin = date_fin - - # Gestion de l'état - etat = scu.EtatJustificatif.get(form["etat"]) - if etat is not None: - justi.etat = etat - else: - raise ScoValueError("État invalide", request.referrer) - - # Gestion de la raison - justi.raison = form["raison"] - - # Gestion des fichiers - files = request.files.getlist("justi_fich") - if len(files) != 0: - files = request.files.values() - - archive_name: str = justi.fichier - # Utilisation de l'archiver de justificatifs - archiver: JustificatifArchiver = JustificatifArchiver() - - for fich in files: - archive_name, _ = archiver.save_justificatif( - justi.etudiant, - filename=fich.filename, - data=fich.stream.read(), - archive_name=archive_name, - user_id=current_user.id, - ) - - justi.fichier = archive_name - - justi.dejustifier_assiduites() - db.session.add(justi) - db.session.commit() - justi.justifier_assiduites() - scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid) + # Si on arrive ici, c'est que l'action n'est pas autorisée + # cette fonction ne sert plus qu'à supprimer ou justifier + flash("Méthode non autorisée", "error") + return redirect(request.referrer) def _preparer_objet( From c8a042cc09adc88cde210ca378883e3f0cf5a86d Mon Sep 17 00:00:00 2001 From: Iziram Date: Tue, 11 Jun 2024 15:07:17 +0200 Subject: [PATCH 3/3] =?UTF-8?q?Assiduit=C3=A9=20:=20balise=20semestre=20no?= =?UTF-8?q?tification=20mail=20closes=20#896?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_abs_notification.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index 54a645e3..0a63fc56 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -31,6 +31,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence. """ + import datetime from typing import Optional @@ -281,6 +282,9 @@ def abs_notification_message( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True ) + # Formsemestre concerné (ex: "BUT Informatique semestre 2") + values["semestre"] = formsemestre.titre_num() + template = prefs["abs_notification_mail_tmpl"] txt = "" if template: