From 21b2e0f582f438e9d7eaab4930db76f4eed4cbd5 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 29 Feb 2024 08:47:03 +0100 Subject: [PATCH 01/11] =?UTF-8?q?Assiduit=C3=A9=20:=20fix=20bug=20module?= =?UTF-8?q?=20selector=20signal=5Fassiduites=5Fgroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/signal_assiduites_group.j2 | 51 ++++++++++--------- app/views/assiduites.py | 6 +-- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/app/templates/assiduites/pages/signal_assiduites_group.j2 b/app/templates/assiduites/pages/signal_assiduites_group.j2 index 12380afaa..4eb21e413 100644 --- a/app/templates/assiduites/pages/signal_assiduites_group.j2 +++ b/app/templates/assiduites/pages/signal_assiduites_group.j2 @@ -1,23 +1,23 @@ {% extends "sco_page.j2" %} {% block title %} - {{title}} +{{title}} {% endblock title %} {% block scripts %} - {{ super() }} - - - - - - +{{ super() }} + + + + + + - + {% endblock scripts %} {% block styles %} - {{ super() }} - - - - - +{{ super() }} + + + + + {% endblock styles %} @@ -80,7 +80,11 @@ {% include "assiduites/widgets/toast.j2" %} {{ minitimeline|safe }} - +
@@ -102,8 +106,7 @@
Groupes : {{grp|safe}}
Date : - +
@@ -162,4 +165,4 @@
-{% endblock app_content %} +{% endblock app_content %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index f892edc6d..16d5c9a06 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -2181,12 +2181,8 @@ def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> s """ # récupération des ues du semestre ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - ues = ntc.get_ues_stat_dict() - modimpls_list: list[dict] = [] - for ue in ues: - # Ajout des moduleimpl de chaque ue dans la liste des moduleimpls - modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"]) + modimpls_list: list[dict] = ntc.get_modimpls_dict() # prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre selected = "" if moduleimpl_id is not None else "selected" From 5158bd0c8fd137cb476895a3d8c87f0e439be1b7 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 29 Feb 2024 14:20:39 +0100 Subject: [PATCH 02/11] =?UTF-8?q?Assiduit=C3=A9=20:=20optimisation=20justi?= =?UTF-8?q?fication=20assiduit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/justificatifs.py | 21 ++---- app/models/assiduites.py | 130 +++++++++++++++------------------- app/scodoc/sco_assiduites.py | 10 +-- app/views/assiduites.py | 13 ++-- tests/unit/test_assiduites.py | 9 ++- tests/unit/test_sco_basic.py | 6 +- 6 files changed, 81 insertions(+), 108 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index f048d1a67..0a5f5350a 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -22,7 +22,6 @@ from app.api import get_model_api_object, tools from app.decorators import permission_required, scodoc from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog from app.models.assiduites import ( - compute_assiduites_justified, get_formsemestre_from_data, ) from app.scodoc.sco_archives_justificatifs import JustificatifArchiver @@ -310,7 +309,6 @@ def justif_create(etudid: int = None, nip=None, ine=None): errors: list[dict] = [] success: list[dict] = [] - justifs: list[Justificatif] = [] # énumération des justificatifs for i, data in enumerate(create_list): @@ -322,11 +320,9 @@ def justif_create(etudid: int = None, nip=None, ine=None): errors.append({"indice": i, "message": obj}) else: success.append({"indice": i, "message": obj}) - justifs.append(justi) + justi.justifier_assiduites() scass.simple_invalidate_cache(data, etud.id) - # Actualisation des assiduités justifiées en fonction de tous les nouveaux justificatifs - compute_assiduites_justified(etud.etudid, justifs) return {"errors": errors, "success": success} @@ -495,6 +491,7 @@ def justif_edit(justif_id: int): return json_error(404, err) # Mise à jour du justificatif + justificatif_unique.dejustifier_assiduites() db.session.add(justificatif_unique) db.session.commit() @@ -511,11 +508,7 @@ def justif_edit(justif_id: int): retour = { "couverture": { "avant": avant_ids, - "apres": compute_assiduites_justified( - justificatif_unique.etudid, - [justificatif_unique], - True, - ), + "apres": justificatif_unique.justifier_assiduites(), } } # Invalide le cache @@ -592,14 +585,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]: # On invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) + # On actualise les assiduités justifiées de l'étudiant concerné + justificatif_unique.dejustifier_assiduites() # On supprime le justificatif db.session.delete(justificatif_unique) - # On actualise les assiduités justifiées de l'étudiant concerné - compute_assiduites_justified( - justificatif_unique.etudid, - Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(), - True, - ) return (200, "OK") diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 8580179bd..db7a25872 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -574,11 +574,7 @@ class Justificatif(ScoDocModel): db.session.delete(self) db.session.commit() # On actualise les assiduités justifiées de l'étudiant concerné - compute_assiduites_justified( - self.etudid, - Justificatif.query.filter_by(etudid=self.etudid).all(), - True, - ) + self.dejustifier_assiduites() def get_fichiers(self) -> tuple[list[str], int]: """Renvoie la liste des noms de fichiers justicatifs @@ -600,6 +596,64 @@ class Justificatif(ScoDocModel): accessible_filenames.append(filename[0]) return accessible_filenames, len(filenames) + def justifier_assiduites( + self, + ) -> list[int]: + """Justifie les assiduités sur la période de validité du justificatif""" + log(f"justifier_assiduites: {self}") + assiduites_justifiees: list[int] = [] + if self.etat != EtatJustificatif.VALIDE: + return [] + # On récupère les assiduités de l'étudiant sur la période donnée + assiduites: Query = self.etudiant.assiduites.filter( + Assiduite.date_debut >= self.date_debut, + Assiduite.date_fin <= self.date_fin, + Assiduite.etat != EtatAssiduite.PRESENT, + ) + # Pour chaque assiduité, on la justifie + for assi in assiduites: + assi.est_just = True + assiduites_justifiees.append(assi.assiduite_id) + db.session.add(assi) + + db.session.commit() + + return assiduites_justifiees + + def dejustifier_assiduites(self) -> list[int]: + """ + Déjustifie les assiduités sur la période du justificatif + """ + assiduites_dejustifiees: list[int] = [] + + # On récupère les assiduités de l'étudiant sur la période donnée + assiduites: Query = self.etudiant.assiduites.filter( + Assiduite.date_debut >= self.date_debut, + Assiduite.date_fin <= self.date_fin, + Assiduite.etat != EtatAssiduite.PRESENT, + ) + assi: Assiduite + for assi in assiduites: + # On récupère les justificatifs qui justifient l'assiduité `assi` + assi_justifs: list[int] = get_justifs_from_date( + self.etudiant.etudid, + assi.date_debut, + assi.date_fin, + long=False, + valid=True, + ) + # Si il n'y a pas d'autre justificatif valide, on déjustifie l'assiduité + if len(assi_justifs) == 0 or ( + len(assi_justifs) == 1 and assi_justifs[0] == self.justif_id + ): + assi.est_just = False + assiduites_dejustifiees.append(assi.assiduite_id) + db.session.add(assi) + + db.session.commit() + + return assiduites_dejustifiees + def is_period_conflicting( date_debut: datetime, @@ -623,72 +677,6 @@ def is_period_conflicting( return count > 0 -def compute_assiduites_justified( - etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False -) -> list[int]: - """ - Args: - etudid (int): l'identifiant de l'étudiant - justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés - reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False. - - Returns: - list[int]: la liste des assiduités qui ont été justifiées. - """ - # TODO à optimiser (car très long avec 40000 assiduités) - # On devrait : - # - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés - # - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie - # - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie - - # Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant - if justificatifs is None: - justificatifs: list[Justificatif] = Justificatif.query.filter_by( - etudid=etudid - ).all() - - # On ne prend que les justificatifs valides - justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE] - - # On récupère les assiduités de l'étudiant - assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) - - assiduites_justifiees: list[int] = [] - - for assi in assiduites: - # On ne justifie pas les Présences - if assi.etat == EtatAssiduite.PRESENT: - continue - - # On récupère les justificatifs qui justifient l'assiduité `assi` - assi_justificatifs = Justificatif.query.filter( - Justificatif.etudid == assi.etudid, - Justificatif.date_debut <= assi.date_debut, - Justificatif.date_fin >= assi.date_fin, - Justificatif.etat == EtatJustificatif.VALIDE, - ).all() - - # Si au moins un justificatif possède une période qui couvre l'assiduité - if any( - assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin - for j in justificatifs + assi_justificatifs - ): - # On justifie l'assiduité - # On ajoute l'id de l'assiduité à la liste des assiduités justifiées - assi.est_just = True - assiduites_justifiees.append(assi.assiduite_id) - db.session.add(assi) - elif reset: - # Si le paramètre reset est Vrai alors les assiduités non justifiées - # sont remise en "non justifiée" - assi.est_just = False - db.session.add(assi) - # On valide la session - db.session.commit() - # On renvoie la liste des assiduite_id des assiduités justifiées - return assiduites_justifiees - - def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]: """ get_assiduites_justif Récupération des justificatifs d'une assiduité diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index 3b9917406..597accb98 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -17,7 +17,7 @@ from app.models import ( ModuleImplInscription, ScoDocSiteConfig, ) -from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified +from app.models.assiduites import Assiduite, Justificatif from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_preferences from app.scodoc import sco_cache @@ -111,9 +111,9 @@ class CountCalculator: evening if evening else ScoDocSiteConfig.get("assi_afternoon_time", "18:00") ) - self.non_work_days: list[scu.NonWorkDays] = ( - scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id) - ) + self.non_work_days: list[ + scu.NonWorkDays + ] = scu.NonWorkDays.get_all_non_work_days(dept_id=g.scodoc_dept_id) # Sera utilisé pour les assiduités longues (> 1 journée) self.nb_heures_par_jour = ( @@ -661,7 +661,7 @@ def create_absence_billet( db.session.add(justi) db.session.commit() - compute_assiduites_justified(etud.id, [justi]) + justi.justifier_assiduites() calculator: CountCalculator = CountCalculator() calculator.compute_assiduites([assiduite_unique]) diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 16d5c9a06..6b2e931a2 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -64,7 +64,7 @@ from app.models import ( ) from app.scodoc.codes_cursus import UE_STANDARD from app.auth.models import User -from app.models.assiduites import get_assiduites_justif, compute_assiduites_justified +from app.models.assiduites import get_assiduites_justif from app.tables.list_etuds import RowEtud, TableEtud import app.tables.liste_assiduites as liste_assi @@ -468,7 +468,7 @@ def _record_assiduite_etud( ) # On met à jour les assiduités en fonction du nouveau justificatif - compute_assiduites_justified(etud.id, [justi]) + justi.justifier_assiduites() # Invalider cache scass.simple_invalidate_cache(ass.to_dict(), etud.id) @@ -778,6 +778,7 @@ def _record_justificatif_etud( form.date_debut.data = dt_debut_tz_server form.date_fin.data = dt_fin_tz_server form.entry_date.data = dt_entry_date_tz_server + justif.dejustifier_assiduites() if justif.edit_from_form(form): message = "Justificatif modifié" @@ -792,7 +793,6 @@ def _record_justificatif_etud( ) else: message = "Pas de modification" - else: justif = Justificatif.create_justificatif( etud, @@ -816,7 +816,7 @@ def _record_justificatif_etud( # pour utiliser le "reset" (remise en "non_just") des assiduités # (à terme, il faudrait ne recalculer que les assiduités impactées) # VOIR TODO dans compute_assiduites_justified - compute_assiduites_justified(etud.id, reset=True) + justif.justifier_assiduites() scass.simple_invalidate_cache(justif.to_dict(), etud.id) flash(message) return True @@ -1595,7 +1595,7 @@ def tableau_assiduite_actions(): user_id=current_user.id, ) - compute_assiduites_justified(objet.etudiant.id, [justificatif_correspondant]) + justificatif_correspondant.justifier_assiduites() scass.simple_invalidate_cache( justificatif_correspondant.to_dict(), objet.etudiant.id ) @@ -1707,9 +1707,10 @@ def _action_modifier_justificatif(justi: Justificatif): justi.fichier = archive_name + justi.dejustifier_assiduites() db.session.add(justi) db.session.commit() - scass.compute_assiduites_justified(justi.etudid, reset=True) + justi.justifier_assiduites() scass.simple_invalidate_cache(justi.to_dict(True), justi.etudid) diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 0cc7ce594..a07eb732a 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -20,7 +20,6 @@ from app.models import ( ModuleImpl, Absence, ) -from app.models.assiduites import compute_assiduites_justified from app.scodoc.sco_exceptions import ScoValueError from tests.unit import sco_fake_gen @@ -498,10 +497,9 @@ def ajouter_justificatifs(etud): ) db.session.add(just_obj) db.session.commit() + just_obj.justifier_assiduites() justificatifs.append(just_obj) - compute_assiduites_justified(etud.etudid, justificatifs) - # Vérification de la création des justificatifs assert [ justi for justi in justificatifs if not isinstance(justi, Justificatif) @@ -1462,7 +1460,7 @@ def test_cas_justificatifs(test_client): etat=scu.EtatJustificatif.VALIDE, ) - compute_assiduites_justified(etud_1.etudid, [justif_2]) + justif_2.justifier_assiduites() assert len(scass.justifies(justif_2)) == 1, "Justification non prise en compte (b1)" @@ -1496,7 +1494,8 @@ def test_cas_justificatifs(test_client): ) # Mise à jour de l'assiduité - compute_assiduites_justified(etud_1.etudid, [justif_3, justif_4]) + justif_3.justifier_assiduites() + justif_4.justifier_assiduites() assert ( len(scass.justifies(justif_3)) == 1 diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index d4cb7a729..e139316d7 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -33,7 +33,6 @@ from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_cursus_dut from app.scodoc import sco_saisie_notes from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, localize_datetime -from app.models.assiduites import compute_assiduites_justified DEPT = TestConfig.DEPT_TEST @@ -267,8 +266,5 @@ def _signal_absences_justificatifs(etudid: int): etat=EtatJustificatif.VALIDE, ) db.session.add(justif) - compute_assiduites_justified( - etud.etudid, - [justif], - ) db.session.commit() + justif.justifier_assiduites() From 75d4c110a8d00faaef60a7511cde603babe5e033 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 1 Mar 2024 11:12:36 +0100 Subject: [PATCH 03/11] =?UTF-8?q?Am=C3=A9liore=20anonymisation=20(users)?= =?UTF-8?q?=20+=20lien=20contact=20+=20cosmetic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/cursus_but.py | 3 +- app/scodoc/sco_dept.py | 2 + sco_version.py | 2 +- tools/anonymize_db.py | 139 ++++++++++++++++++++++++++++++++--------- 4 files changed, 114 insertions(+), 32 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index d55d1e88f..efd375217 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -563,7 +563,8 @@ def formation_semestre_niveaux_warning(formation: Formation, semestre_idx: int) if nb_niveaux_tc != nb_ues_tc: H.append( f"""
  • {nb_niveaux_tc} niveaux de compétences de tronc commun, - mais {nb_ues_tc} UEs de tronc commun !
  • """ + mais {nb_ues_tc} UEs de tronc commun ! (c'est normal si + vous avez des UEs différenciées par parcours)""" ) if H: diff --git a/app/scodoc/sco_dept.py b/app/scodoc/sco_dept.py index 02eba72f8..a2edbd946 100644 --- a/app/scodoc/sco_dept.py +++ b/app/scodoc/sco_dept.py @@ -208,6 +208,8 @@ def index_html(showcodes=0, showsemtable=0): """

    Assistance

    """ diff --git a/sco_version.py b/sco_version.py index 41ebc3033..960e79a53 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.949" +SCOVERSION = "9.6.950" SCONAME = "ScoDoc" diff --git a/tools/anonymize_db.py b/tools/anonymize_db.py index e537d43f0..b2583faec 100755 --- a/tools/anonymize_db.py +++ b/tools/anonymize_db.py @@ -30,12 +30,15 @@ Runned as user "scodoc" with scodoc and postgresql up. -E. Viennet, Jan 2019 -""" +Travaille entièrement au niveau SQL, n'utilise aucun modèle SQLAlchemy. -import psycopg2 +E. Viennet, Jan 2019, Fev 2024 +""" +import random import sys import traceback +import psycopg2 +from psycopg2 import extras def log(msg): @@ -59,9 +62,21 @@ anonymize_false = "FALSE" anonymize_question_str = "'?'" anonymize_null = "NULL" +# --- Listes de noms et prénoms pour remplacer les identités +NOMS = [ + x.strip() + for x in open("/opt/scodoc/tools/fakeportal/nomsprenoms/noms.txt", encoding="utf8") +] +PRENOMS = [ + x.strip() + for x in open( + "/opt/scodoc/tools/fakeportal/nomsprenoms/prenoms.txt", encoding="utf8" + ) +] + # --- Champs à anonymiser (cette configuration pourrait être placé dans # un fichier séparé et le code serait alors générique pour toute base -# posgresql. +# postgresql. # # On essaie de retirer les données personnelles des étudiants et des entreprises # @@ -111,6 +126,26 @@ def anonymize_column(cursor, tablecolumn): cursor.execute(f"UPDATE {table} SET {column} = {anonymized};") +def rename_students(cursor): + """Remet des noms/prenoms fictifs aux étuduiants""" + # Change les noms/prenoms + cursor.execute("""SELECT * FROM "identite";""") + etuds = cursor.fetchall() + for etud in etuds: + nom, prenom = random.choice(NOMS), random.choice(PRENOMS) + cursor.execute( + """UPDATE "identite" + SET nom=%(nom)s, prenom=%(prenom)s + WHERE id=%(id)s + """, + { + "id": etud["id"], + "nom": nom, + "prenom": prenom, + }, + ) + + def anonymize_users(cursor): """Anonymise la table utilisateurs""" log("processing user table") @@ -121,8 +156,51 @@ def anonymize_users(cursor): cursor.execute("""UPDATE "user" SET date_expiration = '2201-12-31';""") cursor.execute("""UPDATE "user" SET token = NULL;""") cursor.execute("""UPDATE "user" SET token_expiration = NULL;""") - cursor.execute("""UPDATE "user" SET nom=CONCAT('nom_', id);""") - cursor.execute("""UPDATE "user" SET prenom=CONCAT('nom_', id);""") + # Change les noms/prenoms/mail + cursor.execute("""SELECT * FROM "user";""") + users = cursor.fetchall() # fetch tout car modifie cette table ds la boucle + used_user_names = {u["user_name"] for u in users} + for user in users: + user_name = user["user_name"] + nom, prenom = random.choice(NOMS), random.choice(PRENOMS) + new_name = (prenom[0] + nom).lower() + # unique ? + while new_name in used_user_names: + new_name += "x" + used_user_names.add(new_name) + print(f"{user_name} > {new_name}") + cursor.execute( + """UPDATE "user" + SET nom=%(nom)s, prenom=%(prenom)s, email=%(email)s, user_name=%(new_name)s + WHERE id=%(id)s + """, + { + "email": f"{prenom}.{nom}@ano.nyme", + "id": user["id"], + "nom": nom, + "prenom": prenom, + "new_name": new_name, + }, + ) + # Change les username: utilisés en référence externe + # dans diverses tables: + for table, field in ( + ("etud_annotations", "author"), + ("scolog", "authenticated_user"), + ("scolar_news", "authenticated_user"), + ("notes_appreciations", "author"), + ("are_historique", "authenticated_user"), + ): + cursor.execute( + f"""UPDATE "{table}" + SET {field}=%(new_name)s + WHERE {field}=%(user_name)s + """, + { + "new_name": new_name, + "user_name": user_name, + }, + ) def anonymize_db(cursor): @@ -131,32 +209,33 @@ def anonymize_db(cursor): anonymize_column(cursor, tablecolumn) -process_users = False -if len(sys.argv) < 2 or len(sys.argv) > 3: - usage() -if len(sys.argv) > 2: - if sys.argv[1] != "--users": +if __name__ == "__main__": + PROCESS_USERS = False + if len(sys.argv) < 2 or len(sys.argv) > 3: usage() - dbname = sys.argv[2] - process_users = True -else: - dbname = sys.argv[1] + if len(sys.argv) > 2: + if sys.argv[1] != "--users": + usage() + dbname = sys.argv[2] + PROCESS_USERS = True + else: + dbname = sys.argv[1] -log(f"\nAnonymizing database {dbname}") -cnx_string = "dbname=" + dbname -try: - cnx = psycopg2.connect(cnx_string) -except Exception as e: - log(f"\n*** Error: can't connect to database {dbname} ***\n") - log(f"""connexion string was "{cnx_string}" """) - traceback.print_exc() + log(f"\nAnonymizing database {dbname}") + cnx_string = "dbname=" + dbname + try: + cnx = psycopg2.connect(cnx_string) + except Exception as e: + log(f"\n*** Error: can't connect to database {dbname} ***\n") + log(f"""connexion string was "{cnx_string}" """) + traceback.print_exc() -cnx.set_session(autocommit=False) -cursor = cnx.cursor() + cnx.set_session(autocommit=False) + cursor = cnx.cursor(cursor_factory=psycopg2.extras.DictCursor) -anonymize_db(cursor) -if process_users: - anonymize_users(cursor) + anonymize_db(cursor) + if PROCESS_USERS: + anonymize_users(cursor) -cnx.commit() -cnx.close() + cnx.commit() + cnx.close() From 169bf17fdd9588eca7e3557c901118d166d703b9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 1 Mar 2024 12:03:00 +0100 Subject: [PATCH 04/11] =?UTF-8?q?Ajout=20colonne=20r=C3=A9f=C3=A9rentiel?= =?UTF-8?q?=20=C3=A0=20la=20table=20des=20formations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 4f9a9bb72..125e8215d 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -527,6 +527,12 @@ def formation_list_table() -> GenTable: "_titre_id": f"""titre-{acronyme_no_spaces}""", "version": formation.version or 0, "commentaire": formation.commentaire or "", + "referentiel": ( + f"""{formation.referentiel_competence.specialite} { + formation.referentiel_competence.get_version()}""" + if formation.referentiel_competence + else "" + ), } # Ajoute les semestres associés à chaque formation: row["formsemestres"] = formation.formsemestres.order_by( @@ -603,6 +609,7 @@ def formation_list_table() -> GenTable: "formation_code", "version", "titre", + "referentiel", "commentaire", "sems_list_txt", ) @@ -615,6 +622,7 @@ def formation_list_table() -> GenTable: "version": "Version", "formation_code": "Code", "sems_list_txt": "Semestres", + "referentiel": "Réf.", } return GenTable( columns_ids=columns_ids, From 411ef8ae0dcf93e89d06e511c1a569163491bc12 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 1 Mar 2024 12:03:19 +0100 Subject: [PATCH 05/11] vocabulaire: portail > passerelle --- app/but/bulletin_but.py | 2 +- app/scodoc/sco_formsemestre_edit.py | 2 +- app/scodoc/sco_preferences.py | 2 +- app/scodoc/sco_recapcomplet.py | 8 ++++---- app/templates/formsemestre_header.j2 | 8 ++++---- app/templates/formsemestre_page_title.j2 | 12 ++++++------ 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index b73b871a8..8eec9ba6a 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -359,7 +359,7 @@ class BulletinBUT: "short" : ne descend pas plus bas que les modules. - Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai - (bulletins non publiés). + (bulletins non publiés sur la passerelle). """ if version not in scu.BULLETINS_VERSIONS_BUT: raise ScoValueError("bulletin_etud: version de bulletin demandée invalide") diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 9321b5119..c8de7b9f5 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -573,7 +573,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N "input_type": "checkbox", "title": "Publication", "allowed_values": ["X"], - "explanation": "publier le bulletin sur le portail étudiants", + "explanation": "publier le bulletin sur la passerelle étudiants", "labels": [""], }, ), diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 3cc147a51..a79c274c8 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -1606,7 +1606,7 @@ class BasePreferences: { "initvalue": 1, "title": "Afficher icône indiquant si les bulletins sont publiés", - "explanation": "décocher si vous n'avez pas de portail étudiant publiant les bulletins", + "explanation": "décocher si vous n'avez pas de passerelle ou portail étudiant publiant les bulletins", "input_type": "boolcheckbox", "labels": ["non", "oui"], "category": "bul", diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 5f1b9c79e..b434c225a 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -81,7 +81,7 @@ def formsemestre_recapcomplet( mode_jury: cache modules, affiche lien saisie decision jury xml_with_decisions: publie décisions de jury dans xml et json - force_publishing: publie les xml et json même si bulletins non publiés + force_publishing: publie les xml et json même si bulletins non publiés (sur la passerelle) selected_etudid: etudid sélectionné (pour scroller au bon endroit) """ if not isinstance(formsemestre_id, int): @@ -398,7 +398,7 @@ def gen_formsemestre_recapcomplet_json( ) -> dict: """JSON export: liste tous les bulletins JSON :param xml_nodate(bool): indique la date courante (attribut docdate) - :param force_publishing: donne les bulletins même si non "publiés sur portail" + :param force_publishing: donne les bulletins même si non "publiés sur la passerelle" :returns: dict """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) @@ -442,13 +442,13 @@ def gen_formsemestre_recapcomplet_json( def formsemestres_bulletins(annee_scolaire): - """Tous les bulletins des semestres publiés des semestres de l'année indiquée. + """Tous les bulletins des semestres de l'année indiquée. :param annee_scolaire(int): année de début de l'année scolaire :returns: JSON """ js_list = [] sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire) - log("formsemestres_bulletins(%s): %d sems" % (annee_scolaire, len(sems))) + log(f"formsemestres_bulletins({annee_scolaire}): {len(sems)} sems") for sem in sems: js_data = gen_formsemestre_recapcomplet_json( sem["formsemestre_id"], force_publishing=False diff --git a/app/templates/formsemestre_header.j2 b/app/templates/formsemestre_header.j2 index b86a85e13..e8c165607 100644 --- a/app/templates/formsemestre_header.j2 +++ b/app/templates/formsemestre_header.j2 @@ -4,7 +4,7 @@
    {{sco.sem.titre}} @@ -13,7 +13,7 @@ {% endif %} {% if sco.sem.modalite %} en {{sco.sem.modalite}}{% endif %} - {{scu.MONTH_NAMES_ABBREV[ sco.sem.date_debut.month - 1]}} {{sco.sem.date_debut.year}} - {{scu.MONTH_NAMES_ABBREV[sco.sem.date_fin.month - 1]}} {{sco.sem.date_fin.year}} @@ -28,9 +28,9 @@ {% if sco.sem.bul_hide_xml %} - {{ scu.icontag("hide_img", border="0", title="Bulletins NON publiés")|safe}} + {{ scu.icontag("hide_img", border="0", title="Bulletins NON publiés sur la passerelle étudiants")|safe}} {% else %} - {{ scu.icontag("eye_img", border="0", title="Bulletins publiés")|safe }} + {{ scu.icontag("eye_img", border="0", title="Bulletins publiés sur la passerelle étudiants")|safe }} {% endif %} {% endif %} diff --git a/app/templates/formsemestre_page_title.j2 b/app/templates/formsemestre_page_title.j2 index 33313b2a9..5674dc32b 100644 --- a/app/templates/formsemestre_page_title.j2 +++ b/app/templates/formsemestre_page_title.j2 @@ -4,7 +4,7 @@
    - {{formsemestre.titre}} {%- if formsemestre.semestre_id != -1 -%} {{formsemestre.mois_debut()}} - {{formsemestre.mois_fin()}}{{formsemestre.responsables_str()}}{{formsemestre.etuds_inscriptions|length}} inscrits {%-if not formsemestre.etat -%} - {{ scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe }} @@ -31,9 +31,9 @@ scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id ) }}">{%- if formsemestre.bul_hide_xml -%} - {{scu.icontag("hide_img", border="0", title="Bulletins NON publiés")|safe}} + {{scu.icontag("hide_img", border="0", title="Bulletins NON publiés sur la passerelle étudiants")|safe}} {%- else -%} - {{scu.icontag("eye_img", border="0", title="Bulletins publiés")|safe}} + {{scu.icontag("eye_img", border="0", title="Bulletins publiés sur la passerelle étudiants")|safe}} {%- endif -%}
    From a79ca4a17d7aae383b51b1057dab04d23453633a Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 1 Mar 2024 12:30:33 +0100 Subject: [PATCH 06/11] =?UTF-8?q?Assiduit=C3=A9=20:=20suppression=20ancien?= =?UTF-8?q?=20tableaux=20(inutilis=C3=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/assiduites/pages/bilan_etud.j2 | 1 - .../assiduites/widgets/tableau_base.j2 | 624 ---------------- .../assiduites/widgets/tableau_justi.j2 | 679 ------------------ 3 files changed, 1304 deletions(-) delete mode 100644 app/templates/assiduites/widgets/tableau_base.j2 delete mode 100644 app/templates/assiduites/widgets/tableau_justi.j2 diff --git a/app/templates/assiduites/pages/bilan_etud.j2 b/app/templates/assiduites/pages/bilan_etud.j2 index 15fed8aad..ae494bdbe 100644 --- a/app/templates/assiduites/pages/bilan_etud.j2 +++ b/app/templates/assiduites/pages/bilan_etud.j2 @@ -66,7 +66,6 @@ Bilan assiduité de {{sco.etud.nomprenom}} {% endblock styles %} {% block app_content %} -{% include "assiduites/widgets/tableau_base.j2" %}

    Bilan de l'assiduité de {{sco.etud.html_link_fiche()|safe}}

    diff --git a/app/templates/assiduites/widgets/tableau_base.j2 b/app/templates/assiduites/widgets/tableau_base.j2 deleted file mode 100644 index 71580a14e..000000000 --- a/app/templates/assiduites/widgets/tableau_base.j2 +++ /dev/null @@ -1,624 +0,0 @@ -
      -
    • Détails
    • -
    • Éditer
    • -
    • Supprimer
    • -
    - -{% include "assiduites/widgets/alert.j2" %} -{% include "assiduites/widgets/prompt.j2" %} - - - - \ No newline at end of file diff --git a/app/templates/assiduites/widgets/tableau_justi.j2 b/app/templates/assiduites/widgets/tableau_justi.j2 deleted file mode 100644 index 06e5c6e51..000000000 --- a/app/templates/assiduites/widgets/tableau_justi.j2 +++ /dev/null @@ -1,679 +0,0 @@ - - - - - - - - - - - - -
    -
    - Début - -
    -
    -
    - Fin - -
    -
    -
    - État - -
    -
    -
    - Raison - -
    -
    -
    - Fichier - -
    -
    -
    -
    - - - From 7eb41fb2eb222532d40ce5bf36caf06450d60be9 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 1 Mar 2024 12:32:44 +0100 Subject: [PATCH 07/11] =?UTF-8?q?Assiduit=C3=A9=20:=20ajout=20test=20api?= =?UTF-8?q?=20manquant=20closes=20#689?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/justificatifs.py | 1 - tests/api/test_api_justificatifs.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 0a5f5350a..b3528362a 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -689,7 +689,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None): @as_json @permission_required(Permission.AbsChange) def justif_remove(justif_id: int = None): - # XXX TODO pas de test unitaire """ Supression d'un fichier ou d'une archive { diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 5e73d2c71..5ae86528b 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -38,6 +38,8 @@ JUSTIFICATIFS_FIELDS = { "external_data": dict, } +DEPT_JUSTIFICATIFS_FIELDS = JUSTIFICATIFS_FIELDS | {"formsemestre": dict | None} + CREATE_FIELD = {"justif_id": int, "couverture": list} BATCH_FIELD = {"errors": list, "success": list} @@ -169,6 +171,32 @@ def test_route_justificatifs(api_headers): check_failure_get(f"/justificatifs/{FAUX}/query?", api_headers) +def test_route_justificatifs_formsemestre(api_headers): + """test de la route /justificatifs/formsemestre/""" + # Bon fonctionnement + + data = GET(path="/justificatifs/formsemestre/1", headers=api_headers) + assert isinstance(data, list) + for just in data: + check_fields(just, JUSTIFICATIFS_FIELDS) + + # Mauvais fonctionnement + check_failure_get(path="/justificatifs/formsemestre/42069", headers=api_headers) + + +def test_justificatifs_dept(api_headers): + """test de la route /justificatifs/dept/""" + # Bon fonctionnement + + data = GET(path="/justificatifs/dept/1", headers=api_headers) + assert isinstance(data, list) + for just in data: + check_fields(just, DEPT_JUSTIFICATIFS_FIELDS) + + # Mauvais fonctionnement + check_failure_get(path="/justificatifs/dept/42069", headers=api_headers) + + def test_route_create(api_admin_headers): """test de la route /justificatif//create""" # -== Unique ==- From 17f8771b0b979d5ee6a7a6467fa57d14072d4623 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 1 Mar 2024 12:33:32 +0100 Subject: [PATCH 08/11] =?UTF-8?q?Assiduit=C3=A9=20:=20fix=20bug=20tableau?= =?UTF-8?q?=20(actualisation=20sur=20les=20lignes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/assiduites/widgets/tableau.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/templates/assiduites/widgets/tableau.j2 b/app/templates/assiduites/widgets/tableau.j2 index 39bbb2f45..1ab881c66 100644 --- a/app/templates/assiduites/widgets/tableau.j2 +++ b/app/templates/assiduites/widgets/tableau.j2 @@ -174,7 +174,7 @@ } window.addEventListener('load', ()=>{ - const table_columns = [...document.querySelectorAll('.external-sort')]; + const table_columns = [...document.querySelectorAll('th.external-sort')]; table_columns.forEach((e)=>e.addEventListener('click', ()=>{ // récupération de l'ordre "ascending" / "descending" From c69e9c34a07ae3c7662c305ccf6388361b03cd02 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 1 Mar 2024 12:35:13 +0100 Subject: [PATCH 09/11] =?UTF-8?q?Assiduit=C3=A9=20:=20fix=20format=20date?= =?UTF-8?q?=20'absences=20du'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/sidebar.j2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/templates/sidebar.j2 b/app/templates/sidebar.j2 index f07e68084..00c612301 100755 --- a/app/templates/sidebar.j2 +++ b/app/templates/sidebar.j2 @@ -56,8 +56,8 @@ Absences {% if sco.etud_cur_sem %} - ({{sco.prefs["assi_metrique"]}}) + ({{sco.prefs["assi_metrique"]}})
    {{'%1.0f'|format(sco.nbabsjust)}} J., {{'%1.0f'|format(sco.nbabsnj)}} N.J.
    {% endif %}
      From 958cf435c8cb47df92a2e53d3e248db2c3c34e49 Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 1 Mar 2024 12:37:38 +0100 Subject: [PATCH 10/11] =?UTF-8?q?Assiduit=C3=A9=20:=20ajout=20tests=20unit?= =?UTF-8?q?aire=20cache=20+=20cas=20justificatifs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/unit/test_assiduites.py | 327 +++++++++++++++--- tests/unit/test_sco_basic.py | 2 +- .../fakedatabase/create_test_api_database.py | 2 +- 3 files changed, 279 insertions(+), 52 deletions(-) diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index a07eb732a..3063c9730 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -72,8 +72,6 @@ def test_general(test_client): verifier_comptage_et_filtrage_assiduites(etuds, moduleimpls[:4], formsemestres) verifier_filtrage_justificatifs(etuds[0], justificatifs) - essais_cache(etuds[0].etudid, formsemestres[:2], moduleimpls) - editer_supprimer_assiduites(etuds, moduleimpls) editer_supprimer_justificatif(etuds[0]) @@ -402,54 +400,6 @@ def _get_justi( ).first() -def essais_cache(etudid, sems: tuple[FormSemestre], moduleimpls: list[ModuleImpl]): - """Vérification des fonctionnalités du cache""" - # TODO faire un test séparé du test_general - # voir test_calcul_assiduites pour faire - - date_deb: str = "2022-09-01T07:00" - date_fin: str = "2023-01-31T19:00" - - assiduites_count_no_cache = scass.get_assiduites_count_in_interval( - etudid, date_deb, date_fin - ) - assiduites_count_cache = scass.get_assiduites_count_in_interval( - etudid, date_deb, date_fin - ) - - assert ( - assiduites_count_cache == assiduites_count_no_cache == (2, 1) - ), "Erreur cache classique" - - assert scass.formsemestre_get_assiduites_count(etudid, sems[0]) == ( - 2, - 1, - ), "Erreur formsemestre_get_assiduites_count (sans module) A" - assert scass.formsemestre_get_assiduites_count(etudid, sems[1]) == ( - 0, - 0, - ), "Erreur formsemestre_get_assiduites_count (sans module) B" - - assert scass.formsemestre_get_assiduites_count( - etudid, sems[0], moduleimpl_id=moduleimpls[0].id - ) == ( - 1, - 1, - ), "Erreur formsemestre_get_assiduites_count (avec module) A" - assert scass.formsemestre_get_assiduites_count( - etudid, sems[0], moduleimpl_id=moduleimpls[1].id - ) == ( - 1, - 0, - ), "Erreur formsemestre_get_assiduites_count (avec module) A" - assert scass.formsemestre_get_assiduites_count( - etudid, sems[0], moduleimpl_id=moduleimpls[2].id - ) == ( - 0, - 0, - ), "Erreur formsemestre_get_assiduites_count (avec module) A" - - def ajouter_justificatifs(etud): """test de l'ajout des justificatifs""" @@ -1414,6 +1364,7 @@ def test_cas_justificatifs(test_client): Tests de certains cas particuliers des justificatifs - Création du justificatif avant ou après assiduité - Assiduité complétement couverte ou non + - Modification de la couverture (edition du justificatif) """ data = _setup_fake_db( @@ -1503,3 +1454,279 @@ def test_cas_justificatifs(test_client): assert ( len(scass.justifies(justif_4)) == 0 ), "Justification complète non prise en compte (c2)" + + # <- Vérification modification de la couverture -> + + # Deux assiduités, 8/01/2024 de 8h à 10h et 14h à 16h + + assi_2: Assiduite = Assiduite.create_assiduite( + etud=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T08:00", True), + date_fin=scu.is_iso_formated("2024-01-08T10:00", True), + etat=scu.EtatAssiduite.ABSENT, + ) + assi_3: Assiduite = Assiduite.create_assiduite( + etud=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T14:00", True), + date_fin=scu.is_iso_formated("2024-01-08T16:00", True), + etat=scu.EtatAssiduite.ABSENT, + ) + + # <=>Justification complète<=> + # les deux assiduités sont couvertes + + justif_5: Justificatif = Justificatif.create_justificatif( + etudiant=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T00:00:00", True), + date_fin=scu.is_iso_formated("2024-01-08T23:59:59", True), + etat=scu.EtatJustificatif.VALIDE, + ) + + # Justification des assiduités + assi_ids: list[int] = justif_5.justifier_assiduites() + assert len(assi_ids) == 2, "Vérification Modification couverture (d1)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d2)" + assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d3)" + assert assi_2.est_just is True, "Vérification Modification couverture (d4)" + assert assi_3.est_just is True, "Vérification Modification couverture (d5)" + + # Déjustification des assiduités + justif_5.dejustifier_assiduites() + assi_ids: list[int] = justif_5.dejustifier_assiduites() + assert len(assi_ids) == 2, "Vérification Modification couverture (d6)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (d7)" + assert assi_3.assiduite_id in assi_ids, "Vérification Modification couverture (d8)" + assert assi_2.est_just is False, "Vérification Modification couverture (d9)" + assert assi_3.est_just is False, "Vérification Modification couverture (d10)" + + # <=>Justification Partielle<=> + # Seule la première assiduité est couverte + + justif_5.date_fin = scu.is_iso_formated("2024-01-08T11:00", True) + db.session.add(justif_5) + db.session.commit() + + assi_ids: list[int] = justif_5.justifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (e1)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e2)" + assert ( + assi_3.assiduite_id not in assi_ids + ), "Vérification Modification couverture (e3)" + assert assi_2.est_just is True, "Vérification Modification couverture (e4)" + assert assi_3.est_just is False, "Vérification Modification couverture (e5)" + + assi_ids: list[int] = justif_5.dejustifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (e6)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (e7)" + assert ( + assi_3.assiduite_id not in assi_ids + ), "Vérification Modification couverture (e3)" + assert assi_2.est_just is False, "Vérification Modification couverture (e8)" + assert assi_3.est_just is False, "Vérification Modification couverture (e9)" + + # <=>Justification Multiple<=> + # Deux justificatifs couvrent une même assiduité + + # on justifie la première assiduité avec le premier justificatif + justif_5.justifier_assiduites() + + # deuxième justificatif + justif_6: Justificatif = Justificatif.create_justificatif( + etudiant=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T08:00", True), + date_fin=scu.is_iso_formated("2024-01-08T10:00", True), + etat=scu.EtatJustificatif.VALIDE, + ) + + assi_ids: list[int] = justif_6.justifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (f1)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f2)" + assert ( + assi_3.assiduite_id not in assi_ids + ), "Vérification Modification couverture (f3)" + assert assi_2.est_just is True, "Vérification Modification couverture (f4)" + assert assi_3.est_just is False, "Vérification Modification couverture (f5)" + + # on déjustifie le justificatif 5 + justif_5.etat = scu.EtatJustificatif.NON_VALIDE + db.session.add(justif_5) + db.session.commit() + + assi_ids: list[int] = justif_5.dejustifier_assiduites() + assert len(assi_ids) == 0, "Vérification Modification couverture (f6)" + assert ( + assi_2.assiduite_id not in assi_ids + ), "Vérification Modification couverture (f7)" + assert assi_2.est_just is True, "Vérification Modification couverture (f8)" + + # on déjustifie le justificatif 6 + justif_6.etat = scu.EtatJustificatif.NON_VALIDE + db.session.add(justif_6) + db.session.commit() + assi_ids: list[int] = justif_6.dejustifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (f9)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (f10)" + assert assi_2.est_just is False, "Vérification Modification couverture (f11)" + + # <=>Justification Chevauchée<=> + # 1 justificatif chevauche une assiduité (8h -> 10h) (9h -> 11h) + + justif_7: Justificatif = Justificatif.create_justificatif( + etudiant=etud_1, + date_debut=scu.is_iso_formated("2024-01-08T09:00", True), + date_fin=scu.is_iso_formated("2024-01-08T11:00", True), + etat=scu.EtatJustificatif.VALIDE, + ) + + assi_ids: list[int] = justif_7.justifier_assiduites() + assert len(assi_ids) == 0, "Vérification Modification couverture (g1)" + assert ( + assi_2.assiduite_id not in assi_ids + ), "Vérification Modification couverture (g2)" + assert assi_2.est_just is False, "Vérification Modification couverture (g3)" + + # Modification pour correspondre à l'assiduité + justif_7.date_debut = scu.is_iso_formated("2024-01-08T08:00", True) + db.session.add(justif_7) + db.session.commit() + + assi_ids: list[int] = justif_7.justifier_assiduites() + assert len(assi_ids) == 1, "Vérification Modification couverture (g4)" + assert assi_2.assiduite_id in assi_ids, "Vérification Modification couverture (g5)" + assert assi_2.est_just is True, "Vérification Modification couverture (g6)" + + +def test_cache_assiduites(test_client): + """Vérification du bon fonctionnement du cache des assiduités""" + + data = _setup_fake_db( + [("2024-01-01", "2024-06-30"), ("2024-07-01", "2024-12-31")], + 1, + 1, + ) + + formsemestre1: FormSemestre = data["formsemestres"][0] + formsemestre2: FormSemestre = data["formsemestres"][1] + + moduleimpl: ModuleImpl = data["moduleimpls"][0] + etud: Identite = data["etuds"][0] + + # Création des assiduités + assiduites: list[dict] = [ + # Semestre 1 + { + "date_debut": "2024-01-08T08:00", + "date_fin": "2024-01-08T10:00", + "moduleimpl": moduleimpl, + }, + { + "date_debut": "2024-01-08T14:00", + "date_fin": "2024-01-08T16:00", + "moduleimpl": moduleimpl, + }, + { + "date_debut": "2024-01-09T08:00", + "date_fin": "2024-01-09T10:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-01-09T14:00", + "date_fin": "2024-01-09T16:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-01-10T08:00", + "date_fin": "2024-01-10T10:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-01-10T14:00", + "date_fin": "2024-01-10T16:00", + "moduleimpl": moduleimpl, + }, + # Semestre 2 + { + "date_debut": "2024-07-09T14:00", + "date_fin": "2024-07-09T16:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-07-10T08:00", + "date_fin": "2024-07-10T10:00", + "moduleimpl": None, + }, + { + "date_debut": "2024-07-10T14:00", + "date_fin": "2024-07-10T16:00", + "moduleimpl": None, + }, + ] + + justificatifs: list[dict] = [ + { + "date_debut": "2024-01-10T00:00", + "date_fin": "2024-01-10T23:59", + }, + { + "date_debut": "2024-07-09T00:00", + "date_fin": "2024-07-09T23:59", + }, + ] + + # On ajoute les assiduités et les justificatifs + + for assi in assiduites: + Assiduite.create_assiduite( + etud=etud, + date_debut=scu.is_iso_formated(assi["date_debut"], True), + date_fin=scu.is_iso_formated(assi["date_fin"], True), + moduleimpl=assi["moduleimpl"], + etat=scu.EtatAssiduite.ABSENT, + ) + + for justi in justificatifs: + Justificatif.create_justificatif( + etudiant=etud, + date_debut=scu.is_iso_formated(justi["date_debut"], True), + date_fin=scu.is_iso_formated(justi["date_fin"], True), + etat=scu.EtatJustificatif.VALIDE, + ).justifier_assiduites() + + # Premier semestre 4nj / 2j / 6t + assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6) + assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6) + + # ModuleImpl 2nj / 1j / 3t + assert scass.formsemestre_get_assiduites_count( + etud.id, formsemestre1, moduleimpl.id + ) == (2, 1, 3) + # Deuxième semestre 2nj / 1j / 3t + assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3) + + # On supprime la première assiduité (sans invalider le cache) + assi: Assiduite = Assiduite.query.filter_by(etudid=etud.id).first() + db.session.delete(assi) + db.session.commit() + + # Premier semestre 4nj / 2j / 6t (Identique car cache) + assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (4, 2, 6) + assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (4, 2, 6) + # ModuleImpl 1nj / 1j / 2t (Change car non cache) + assert scass.formsemestre_get_assiduites_count( + etud.id, formsemestre1, moduleimpl.id + ) == (1, 1, 2) + # Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié) + assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3) + + # On invalide maintenant le cache + scass.invalidate_assiduites_count(etud.id, formsemestre1.to_dict()) + + # Premier semestre 3nj / 2j / 5t (Change car cache invalidé) + assert scass.get_assiduites_count(etud.id, formsemestre1.to_dict()) == (3, 2, 5) + assert scass.formsemestre_get_assiduites_count(etud.id, formsemestre1) == (3, 2, 5) + # ModuleImpl 1nj / 1j / 2t (Ne change pas car pas de changement) + assert scass.formsemestre_get_assiduites_count( + etud.id, formsemestre1, moduleimpl.id + ) == (1, 1, 2) + # Deuxième semestre 2nj / 1j / 3t (Identique car cache et non modifié) + assert scass.get_assiduites_count(etud.id, formsemestre2.to_dict()) == (2, 1, 3) diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index e139316d7..b3bba2e20 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -191,7 +191,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre: etudid = etuds[0]["etudid"] _signal_absences_justificatifs(etudid) - nbabs, nbabsjust = scass.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = scass.get_assiduites_count(etudid, sem) assert nbabs == 6, f"incorrect nbabs ({nbabs})" assert nbabsjust == 2, f"incorrect nbabsjust ({nbabsjust})" diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 5b3e96a2c..cd5182bee 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -396,7 +396,7 @@ def ajouter_assiduites_justificatifs(formsemestre: FormSemestre): for etud in formsemestre.etuds: base_date = datetime.datetime( - 2022, 9, [5, 12, 19, 26][random.randint(0, 3)], 8, 0, 0 + 2021, 9, [6, 13, 20, 27][random.randint(0, 3)], 8, 0, 0 ) base_date = localize_datetime(base_date) From 0332553587b0f22502f83603a1f8d45308a79d6b Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 1 Mar 2024 12:40:05 +0100 Subject: [PATCH 11/11] =?UTF-8?q?Assiduit=C3=A9=20:=20correction=20bug=20c?= =?UTF-8?q?ache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 12 ++++++------ app/but/bulletin_but_xml_compat.py | 2 +- app/models/formsemestre.py | 2 +- app/scodoc/html_sidebar.py | 3 +-- app/scodoc/sco_abs_notification.py | 2 +- app/scodoc/sco_assiduites.py | 21 +++++++++++---------- app/scodoc/sco_bulletins.py | 4 ++-- app/scodoc/sco_bulletins_json.py | 2 +- app/scodoc/sco_bulletins_xml.py | 4 ++-- app/scodoc/sco_formsemestre_validation.py | 4 ++-- app/scodoc/sco_poursuite_dut.py | 6 ++++-- app/tables/recap.py | 14 +++++++------- app/views/__init__.py | 4 ++-- app/views/notes.py | 8 ++++++-- 14 files changed, 47 insertions(+), 41 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 8eec9ba6a..ab4227b0d 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -393,7 +393,7 @@ class BulletinBUT: else: etud_ues_ids = res.etud_ues_ids(etud.id) - nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + nbabsnj, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id) etud_groups = sco_groups.get_etud_formsemestre_groups( etud, formsemestre, only_to_show=True ) @@ -408,7 +408,7 @@ class BulletinBUT: } if self.prefs["bul_show_abs"]: semestre_infos["absences"] = { - "injustifie": nbabs - nbabsjust, + "injustifie": nbabsnj, "total": nbabs, "metrique": { "H.": "Heure(s)", @@ -525,7 +525,7 @@ class BulletinBUT: d["demission"] = "" # --- Absences - d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id) + _, d["nbabsjust"], d["nbabs"] = self.res.formsemestre.get_abs_count(etud.id) # --- Decision Jury infos, _ = sco_bulletins.etud_descr_situation_semestre( @@ -540,9 +540,9 @@ class BulletinBUT: d.update(infos) # --- Rangs - d["rang_nt"] = ( - f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" - ) + d[ + "rang_nt" + ] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" d["rang_txt"] = "Rang " + d["rang_nt"] d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 07522f80c..fb9af2056 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -241,7 +241,7 @@ def bulletin_but_xml_compat( # --- Absences if sco_preferences.get_preference("bul_show_abs", formsemestre_id): - nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + _, nbabsjust, nbabs = formsemestre.get_abs_count(etud.id) doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust))) # -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py --------- diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 09c1d3056..5541d1780 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -875,7 +875,7 @@ class FormSemestre(db.Model): def get_abs_count(self, etudid): """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs, nb abs justifiées) + tuple (nb abs non just, nb abs justifiées, nb abs total) Utilise un cache. """ from app.scodoc import sco_assiduites diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index f1c8f8356..2d351e628 100755 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -175,10 +175,9 @@ def sidebar(etudid: int = None): inscription = etud.inscription_courante() if inscription: formsemestre = inscription.formsemestre - nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count( + nbabsnj, nbabsjust, _ = sco_assiduites.formsemestre_get_assiduites_count( etudid, formsemestre ) - nbabsnj = nbabs - nbabsjust H.append( f""" tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs non justifiées, nb abs justifiées, nb abs total) Utilise un cache. """ metrique = sco_preferences.get_preference("assi_metrique", sem["formsemestre_id"]) @@ -687,17 +687,17 @@ def formsemestre_get_assiduites_count( etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None ) -> tuple[int, int]: """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs non justifiées, nb abs justifiées, nb abs total) Utilise un cache. """ metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id) return get_assiduites_count_in_interval( etudid, date_debut=scu.localize_datetime( - datetime.combine(formsemestre.date_debut, time(8, 0)) + datetime.combine(formsemestre.date_debut, time(0, 0)) ), date_fin=scu.localize_datetime( - datetime.combine(formsemestre.date_fin, time(18, 0)) + datetime.combine(formsemestre.date_fin, time(23, 0)) ), metrique=scu.translate_assiduites_metric(metrique), moduleimpl_id=moduleimpl_id, @@ -714,12 +714,12 @@ def get_assiduites_count_in_interval( moduleimpl_id: int = None, ): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: - tuple (nb abs, nb abs justifiées) + tuple (nb abs non justifiées, nb abs justifiées, nb abs total) On peut spécifier les dates comme datetime ou iso. Utilise un cache. """ - date_debut_iso = date_debut_iso or date_debut.isoformat() - date_fin_iso = date_fin_iso or date_fin.isoformat() + date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d") + date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d") key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites" r = sco_cache.AbsSemEtudCache.get(key) @@ -744,9 +744,10 @@ def get_assiduites_count_in_interval( if not ans: log("warning: get_assiduites_count failed to cache") - nb_abs: dict = r["absent"][metrique] - nb_abs_just: dict = r["absent_just"][metrique] - return (nb_abs, nb_abs_just) + nb_abs: int = r["absent"][metrique] + nb_abs_nj: int = r["absent_non_just"][metrique] + nb_abs_just: int = r["absent_just"][metrique] + return (nb_abs_nj, nb_abs_just, nb_abs) def invalidate_assiduites_count(etudid: int, sem: dict): diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 1b70d3858..d1c32795d 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -196,7 +196,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): pid = partition["partition_id"] partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid) # --- Absences - I["nbabs"], I["nbabsjust"] = sco_assiduites.get_assiduites_count(etudid, nt.sem) + _, I["nbabsjust"], I["nbabs"] = sco_assiduites.get_assiduites_count(etudid, nt.sem) # --- Decision Jury infos, dpv = etud_descr_situation_semestre( @@ -471,7 +471,7 @@ def _ue_mod_bulletin( ) # peut etre 'NI' is_malus = mod["module"]["module_type"] == ModuleType.MALUS if bul_show_abs_modules: - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) mod_abs = [nbabs, nbabsjust] mod["mod_abs_txt"] = scu.fmt_abs(mod_abs) else: diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 0481e6f9c..a7848b39e 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -296,7 +296,7 @@ def formsemestre_bulletinetud_published_dict( # --- Absences if prefs["bul_show_abs"]: - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust) # --- Décision Jury diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index 25f2cfa64..77f95ac28 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -260,7 +260,7 @@ def make_xml_formsemestre_bulletinetud( numero=str(mod["numero"]), titre=quote_xml_attr(mod["titre"]), abbrev=quote_xml_attr(mod["abbrev"]), - code_apogee=quote_xml_attr(mod["code_apogee"]) + code_apogee=quote_xml_attr(mod["code_apogee"]), # ects=ects ects des modules maintenant inutilisés ) x_ue.append(x_mod) @@ -347,7 +347,7 @@ def make_xml_formsemestre_bulletinetud( # --- Absences if sco_preferences.get_preference("bul_show_abs", formsemestre_id): - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) + _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust))) # --- Decision Jury if ( diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index df4770fa3..27d99fffd 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -722,8 +722,8 @@ def formsemestre_recap_parcours_table( f"""{scu.fmt_note(nt.get_etud_moy_gen(etudid))}""" ) # Absences (nb d'abs non just. dans ce semestre) - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, sem) - H.append(f"""{nbabs - nbabsjust}""") + nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0] + H.append(f"""{nbabsnj}""") # UEs for ue in ues: diff --git a/app/scodoc/sco_poursuite_dut.py b/app/scodoc/sco_poursuite_dut.py index c271628a2..475d59808 100644 --- a/app/scodoc/sco_poursuite_dut.py +++ b/app/scodoc/sco_poursuite_dut.py @@ -105,7 +105,9 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict: rangs.append(["rang_" + code_module, rang_module]) # Absences - nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem) + nbabsnj, nbabsjust, _ = sco_assiduites.get_assiduites_count( + etudid, nt.sem + ) # En BUT, prend tout, sinon ne prend que les semestre validés par le jury if nt.is_apc or ( dec @@ -125,7 +127,7 @@ def etud_get_poursuite_info(sem: dict, etud: dict) -> dict: ("date_debut", s["date_debut"]), ("date_fin", s["date_fin"]), ("periode", "%s - %s" % (s["mois_debut"], s["mois_fin"])), - ("AbsNonJust", nbabs - nbabsjust), + ("AbsNonJust", nbabsnj), ("AbsJust", nbabsjust), ] # ajout des 2 champs notes des modules et classement dans chaque module diff --git a/app/tables/recap.py b/app/tables/recap.py index f26535edc..0c853c352 100644 --- a/app/tables/recap.py +++ b/app/tables/recap.py @@ -620,7 +620,7 @@ class RowRecap(tb.Row): def add_abs(self): "Ajoute les colonnes absences" # Absences (nb d'abs non just. dans ce semestre) - nbabs, nbabsjust = self.table.res.formsemestre.get_abs_count(self.etud.id) + _, nbabsjust, nbabs = self.table.res.formsemestre.get_abs_count(self.etud.id) self.add_cell("nbabs", "Abs", f"{nbabs:1.0f}", "abs", raw_content=nbabs) self.add_cell( "nbabsjust", "Just.", f"{nbabsjust:1.0f}", "abs", raw_content=nbabsjust @@ -691,9 +691,9 @@ class RowRecap(tb.Row): self.add_ue_modimpls_cols(ue, ue_status["is_capitalized"]) self.nb_ues_etud_parcours = len(res.etud_parcours_ues_ids(etud.id)) - ue_valid_txt = ue_valid_txt_html = ( - f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" - ) + ue_valid_txt = ( + ue_valid_txt_html + ) = f"{self.nb_ues_validables}/{self.nb_ues_etud_parcours}" if self.nb_ues_warning: ue_valid_txt_html += " " + scu.EMO_WARNING cell_class = "" @@ -717,9 +717,9 @@ class RowRecap(tb.Row): # sous-classé par JuryRow pour ajouter les codes table: TableRecap = self.table formsemestre: FormSemestre = table.res.formsemestre - table.group_titles["col_ue"] = ( - f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}" - ) + table.group_titles[ + "col_ue" + ] = f"UEs du S{formsemestre.semestre_id} {formsemestre.annee_scolaire()}" col_id = f"moy_ue_{ue.id}" val = ( ue_status["moy"] diff --git a/app/views/__init__.py b/app/views/__init__.py index 890fb63eb..b28a4e57c 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -74,8 +74,9 @@ class ScoData: if ins: self.etud_cur_sem = ins.formsemestre ( - self.nbabs, + self.nbabsnj, self.nbabsjust, + self.nbabs, ) = sco_assiduites.get_assiduites_count_in_interval( etud.id, self.etud_cur_sem.date_debut.isoformat(), @@ -84,7 +85,6 @@ class ScoData: sco_preferences.get_preference("assi_metrique") ), ) - self.nbabsnj = self.nbabs - self.nbabsjust else: self.etud_cur_sem = None else: diff --git a/app/views/notes.py b/app/views/notes.py index 717131db3..570b64f52 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1187,14 +1187,18 @@ def view_module_abs(moduleimpl_id, fmt="html"): rows = [] for etud in inscrits: - nb_abs, nb_abs_just = sco_assiduites.formsemestre_get_assiduites_count( + ( + nb_abs_nj, + nb_abs_just, + nb_abs, + ) = sco_assiduites.formsemestre_get_assiduites_count( etud.id, modimpl.formsemestre, moduleimpl_id=modimpl.id ) rows.append( { "nomprenom": etud.nomprenom, "just": nb_abs_just, - "nojust": nb_abs - nb_abs_just, + "nojust": nb_abs_nj, "total": nb_abs, "_nomprenom_target": url_for( "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id