From 7659bcb488e1f90fcc86f82a0aebf431c69c135e Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 18 Jan 2024 17:05:43 +0100 Subject: [PATCH] Assiduites : WIP todos --- app/forms/main/config_assiduites.py | 81 ++++------ app/scodoc/sco_archives_justificatifs.py | 24 ++- app/scodoc/sco_assiduites.py | 29 ++-- app/scodoc/sco_utils.py | 7 +- app/static/js/assiduites.js | 36 ++--- app/tables/visu_assiduites.py | 51 ++++--- app/templates/assiduites/pages/bilan_etud.j2 | 143 ++++++++---------- .../assiduites/pages/config_assiduites.j2 | 18 ++- app/templates/assiduites/widgets/conflict.j2 | 17 ++- app/templates/assiduites/widgets/tableau.j2 | 49 ++++++ app/views/absences.py | 3 +- app/views/assiduites.py | 4 +- app/views/scodoc.py | 10 +- 13 files changed, 257 insertions(+), 215 deletions(-) diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py index eec5623e..2ab33165 100644 --- a/app/forms/main/config_assiduites.py +++ b/app/forms/main/config_assiduites.py @@ -34,52 +34,11 @@ import re from flask_wtf import FlaskForm from wtforms import DecimalField, SubmitField, ValidationError from wtforms.fields.simple import StringField -from wtforms.validators import Optional +from wtforms.validators import Optional, Length from wtforms.widgets import TimeInput -class TimeField(StringField): - """HTML5 time input. - tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f - """ - - widget = TimeInput() - - def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs): - super(TimeField, self).__init__(label, validators, **kwargs) - self.fmt = fmt - self.data = None - - def _value(self): - if self.raw_data: - return " ".join(self.raw_data) - if self.data and isinstance(self.data, str): - self.data = datetime.time(*map(int, self.data.split(":"))) - return self.data and self.data.strftime(self.fmt) or "" - - def process_formdata(self, valuelist): - if valuelist: - time_str = " ".join(valuelist) - try: - components = time_str.split(":") - hour = 0 - minutes = 0 - seconds = 0 - if len(components) in range(2, 4): - hour = int(components[0]) - minutes = int(components[1]) - - if len(components) == 3: - seconds = int(components[2]) - else: - raise ValueError - self.data = datetime.time(hour, minutes, seconds) - except ValueError as exc: - self.data = None - raise ValueError(self.gettext("Not a valid time string")) from exc - - def check_tick_time(form, field): """Le tick_time doit être entre 0 et 60 minutes""" if field.data < 1 or field.data > 59: @@ -118,14 +77,36 @@ def check_ics_regexp(form, field): class ConfigAssiduitesForm(FlaskForm): "Formulaire paramétrage Module Assiduité" - - assi_morning_time = TimeField( - "Début de la journée" - ) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm - assi_lunch_time = TimeField( - "Heure de midi (date pivot entre matin et après-midi)" - ) # TODO - assi_afternoon_time = TimeField("Fin de la journée") # TODO + assi_morning_time = StringField( + "Début de la journée", + default="", + validators=[Length(max=5)], + render_kw={ + "class": "timepicker", + "size": 5, + "id": "assi_morning_time", + }, + ) + assi_lunch_time = StringField( + "Heure de midi (date pivot entre matin et après-midi)", + default="", + validators=[Length(max=5)], + render_kw={ + "class": "timepicker", + "size": 5, + "id": "assi_lunch_time", + }, + ) + assi_afternoon_time = StringField( + "Fin de la journée", + validators=[Length(max=5)], + default="", + render_kw={ + "class": "timepicker", + "size": 5, + "id": "assi_afternoon_time", + }, + ) assi_tick_time = DecimalField( "Granularité de la timeline (temps en minutes)", diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index b0fb1d3e..abdb9fff 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -17,7 +17,15 @@ from app import log class Trace: """gestionnaire de la trace des fichiers justificatifs - XXX TODO à documenter: rôle et format des fichier strace + + Role des fichiers traces : + - Sauvegarder la date de dépot du fichier + - Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif) + - Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView) + + _trace.csv : + nom_fichier_srv,datetime_depot,datetime_suppr,user_id + """ def __init__(self, path: str) -> None: @@ -39,7 +47,7 @@ class Trace: continue entry_date: datetime = is_iso_formated(csv[1], True) delete_date: datetime = is_iso_formated(csv[2], True) - user_id = csv[3] + user_id = csv[3].strip() self.content[fname] = [entry_date, delete_date, user_id] if os.path.isfile(self.path): @@ -84,7 +92,14 @@ class Trace: self, fnames: list[str] = None ) -> dict[str, list[datetime, datetime, str]]: """Récupère la trace pour les noms de fichiers. - si aucun nom n'est donné, récupère tous les fichiers""" + si aucun nom n'est donné, récupère tous les fichiers + + retour : + { + "nom_fichier_srv": [datetime_depot, datetime_suppr/None, user_id], + ... + } + """ if fnames is None: return self.content @@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver): filenames = self.list_archive(archive_id, dept_id=etud.dept_id) trace: Trace = Trace(archive_id) traced = trace.get_trace(filenames) - - return [(key, value[2]) for key, value in traced.items()] + return [(key, value[2]) for key, value in traced.items() if value is not None] def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str): """ diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 70121a17..1421ff28 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -450,8 +450,6 @@ def filter_by_date( if date_fin is None: date_fin = datetime.max - date_deb = scu.localize_datetime(date_deb) # TODO A modifier (timezone ?) - date_fin = scu.localize_datetime(date_fin) if not strict: return collection.filter( collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb @@ -558,15 +556,19 @@ def get_all_justified( return after -def create_absence( +def create_absence_billet( date_debut: datetime, date_fin: datetime, etudid: int, description: str = None, est_just: bool = False, ) -> int: - """TODO: doc, dire quand l'utiliser""" - # TODO + """ + Permet de rapidement créer une absence. + **UTILISÉ UNIQUEMENT POUR LES BILLETS** + Ne pas utiliser autre par. + TALK: Vérifier si nécessaire + """ etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404() assiduite_unique: Assiduite = Assiduite.create_assiduite( etud=etud, @@ -648,8 +650,7 @@ def get_assiduites_count_in_interval( """ date_debut_iso = date_debut_iso or date_debut.isoformat() date_fin_iso = date_fin_iso or date_fin.isoformat() - # TODO Question: pourquoi ne pas cacher toutes les métriques, si l'API les veut toutes ? - key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites" + key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites" r = sco_cache.AbsSemEtudCache.get(key) if not r or moduleimpl_id is not None: @@ -666,24 +667,24 @@ def get_assiduites_count_in_interval( calculator: CountCalculator = CountCalculator() calculator.compute_assiduites(assiduites) calcul: dict = calculator.to_dict(only_total=False) - nb_abs: dict = calcul["absent"][metrique] - nb_abs_just: dict = calcul["absent_just"][metrique] - r = (nb_abs, nb_abs_just) + r = calcul if moduleimpl_id is None: ans = sco_cache.AbsSemEtudCache.set(key, r) if not ans: log("warning: get_assiduites_count failed to cache") - return r + + nb_abs: dict = r["absent"][metrique] + nb_abs_just: dict = r["absent_just"][metrique] + return (nb_abs, nb_abs_just) def invalidate_assiduites_count(etudid: int, sem: dict): """Invalidate (clear) cached counts""" date_debut = sem["date_debut_iso"] date_fin = sem["date_fin_iso"] - for met in scu.AssiduitesMetrics.TAG: - key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites" - sco_cache.AbsSemEtudCache.delete(key) + key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites" + sco_cache.AbsSemEtudCache.delete(key) # Non utilisé diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 4ba283e1..a6d5d8b9 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -299,8 +299,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No def localize_datetime(date: datetime.datetime or str) -> datetime.datetime: - """Ajoute un timecode UTC à la date donnée. - XXX semble faire autre chose... TODO fix this comment + """Transforme une date sans offset en une date avec offset + Tente de mettre l'offset de la timezone du serveur (ex : UTC+1) + Si erreur, mettra l'offset UTC + + TODO : vérifier puis supprimer l'auto conversion str-> datetime """ if isinstance(date, str): date = is_iso_formated(date, convert=True) diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index b1522e7d..bd22ecb2 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -1282,19 +1282,14 @@ function getAllAssiduitesFromEtud( .replace("°", courant ? "&courant" : "") : "" }`; - //TODO Utiliser async_get au lieu de jquery - $.ajax({ - async: true, - type: "GET", - url: url_api, - success: (data, status) => { - if (status === "success") { - assiduites[etudid] = data; - action(data); - } + async_get( + url_api, + (data) => { + assiduites[etudid] = data; + action(data); }, - error: () => {}, - }); + (_) => {} + ); } /** @@ -1864,18 +1859,13 @@ function getAllJustificatifsFromEtud( order ? "/query?order°".replace("°", courant ? "&courant" : "") : "" }`; - //TODO Utiliser async_get au lieu de jquery - $.ajax({ - async: true, - type: "GET", - url: url_api, - success: (data, status) => { - if (status === "success") { - action(data); - } + async_get( + url_api, + (data) => { + action(data); }, - error: () => {}, - }); + () => {} + ); } function deleteJustificatif(justif_id) { diff --git a/app/tables/visu_assiduites.py b/app/tables/visu_assiduites.py index 0b7ee3fc..ed37c287 100644 --- a/app/tables/visu_assiduites.py +++ b/app/tables/visu_assiduites.py @@ -129,41 +129,44 @@ class RowAssi(tb.Row): ) def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]: - # XXX TODO @iziram commentaire sur la fonction et la var. retour + """ + Renvoie le comptage (dans la métrique du département) des différents états d'assiduité d'un étudiant + + Returns : + { + "" : [, , ] + } + + """ + + # Préparation du retour retour: dict[str, tuple[str, float, float]] = { "absent": ["Absences", 0.0, 0.0], "retard": ["Retards", 0.0, 0.0], "present": ["Présences", 0.0, 0.0], } + # Récupération de la métrique du département assi_metric = scu.translate_assiduites_metric( sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id), ) + compte_etat: dict[str, dict] = scass.get_assiduites_stats( + assiduites=etud.assiduites, + metric=assi_metric, + filtered={ + "date_debut": self.dates[0], + "date_fin": self.dates[1], + "etat": "absent,present,retard", # pour tout compter d'un coup + "split": 1, # afin d'avoir la division des stats en état, etatjust, etatnonjust + }, + ) + + # Pour chaque état on mets à jour les valeurs de retour for etat, valeur in retour.items(): - compte_etat = scass.get_assiduites_stats( - assiduites=etud.assiduites, - metric=assi_metric, - filtered={ - "date_debut": self.dates[0], - "date_fin": self.dates[1], - "etat": etat, - }, - ) - - compte_etat_just = scass.get_assiduites_stats( - assiduites=etud.assiduites, - metric=assi_metric, - filtered={ - "date_debut": self.dates[0], - "date_fin": self.dates[1], - "etat": etat, - "est_just": True, - }, - ) - - valeur[1] = compte_etat[assi_metric] - valeur[2] = compte_etat_just[assi_metric] + valeur[1] = compte_etat[etat][assi_metric] + if etat != "present": + valeur[2] = compte_etat[etat]["justifie"][assi_metric] return retour diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 02ba9591..7c1f1c7e 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -28,7 +28,7 @@

Absences et retards non justifiés

- {# XXX XXX XXX #} + {# TODO Utiliser python tableau plutot que js tableau #}
Attention, cette page utilise des couleurs et conventions différentes de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
@@ -111,89 +111,76 @@ } - function getAssiduitesCount(dateDeb, dateFin, query) { - const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`; + function getAssiduitesCount(dateDeb, dateFin, action) { + const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`; //Utiliser async_get au lieu de Jquery - return $.ajax({ - async: true, - type: "GET", - url: url_api, - success: (data, status) => { - if (status === "success") { - } + async_get( + url_api, + action, + ()=>{}, + ); + } + + function showStats(data){ + const counter = { + "present": { + "total": data["present"], }, - error: () => { }, + "retard": { + "total": data["retard"], + "justi": data["retard"]["justifie"], + }, + "absent": { + "total": data["absent"], + "justi": data["absent"]["justifie"], + } + } + + const values = document.querySelector('.stats-values'); + values.innerHTML = ""; + + Object.keys(counter).forEach((key) => { + const item = document.createElement('div'); + item.classList.add('stats-values-item'); + + const div = document.createElement('div'); + div.classList.add('stats-values-part'); + + const withJusti = (key, metric) => { + if (key == "present") return ""; + return ` dont ${counter[key].justi[metric]} justifiées` + } + + const heure = document.createElement('span'); + heure.textContent = `${counter[key].total.heure} heure(s)${withJusti(key, "heure")}`; + + const demi = document.createElement('span'); + demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`; + + const jour = document.createElement('span'); + jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`; + + div.append(jour, demi, heure); + + const title = document.createElement('h5'); + title.textContent = key.capitalize(); + + item.append(title, div) + + values.appendChild(item); }); + + const nbAbs = data["absent"]["non_justifie"][assi_metric]; + if (nbAbs > assi_seuil) { + document.querySelector('.alerte').classList.remove('invisible'); + document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})` + } else { + document.querySelector('.alerte').classList.add('invisible'); + } } function countAssiduites(dateDeb, dateFin) { - //TODO Utiliser Fetch when plutot que jquery - $.when( - getAssiduitesCount(dateDeb, dateFin, `etat=present`), - getAssiduitesCount(dateDeb, dateFin, `etat=retard`), - getAssiduitesCount(dateDeb, dateFin, `etat=retard&est_just=v`), - getAssiduitesCount(dateDeb, dateFin, `etat=absent`), - getAssiduitesCount(dateDeb, dateFin, `etat=absent&est_just=v`), - ).then( - (pt, rt, rj, at, aj) => { - const counter = { - "present": { - "total": pt[0], - }, - "retard": { - "total": rt[0], - "justi": rj[0], - }, - "absent": { - "total": at[0], - "justi": aj[0], - } - } - - const values = document.querySelector('.stats-values'); - values.innerHTML = ""; - - Object.keys(counter).forEach((key) => { - const item = document.createElement('div'); - item.classList.add('stats-values-item'); - - const div = document.createElement('div'); - div.classList.add('stats-values-part'); - - const withJusti = (key, metric) => { - if (key == "present") return ""; - return ` dont ${counter[key].justi[metric]} justifiées` - } - - const heure = document.createElement('span'); - heure.textContent = `${counter[key].total.heure} heure(s)${withJusti(key, "heure")}`; - - const demi = document.createElement('span'); - demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`; - - const jour = document.createElement('span'); - jour.textContent = `${counter[key].total.journee} journée(s)${withJusti(key, "journee")}`; - - div.append(jour, demi, heure); - - const title = document.createElement('h5'); - title.textContent = key.capitalize(); - - item.append(title, div) - - values.appendChild(item); - }); - - const nbAbs = counter.absent.total[assi_metric] - counter.absent.justi[assi_metric]; - if (nbAbs > assi_seuil) { - document.querySelector('.alerte').classList.remove('invisible'); - document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})` - } else { - document.querySelector('.alerte').classList.add('invisible'); - } - } - ); - + getAssiduitesCount(dateDeb, dateFin, showStats); } function removeAllAssiduites() { diff --git a/app/templates/assiduites/pages/config_assiduites.j2 b/app/templates/assiduites/pages/config_assiduites.j2 index 1b71738e..b69f5a2e 100644 --- a/app/templates/assiduites/pages/config_assiduites.j2 +++ b/app/templates/assiduites/pages/config_assiduites.j2 @@ -3,6 +3,7 @@ {% block styles %} {{super()}} +