From 943055b32855487d2637f24c0913e989f1450aef Mon Sep 17 00:00:00 2001 From: Iziram Date: Fri, 27 Oct 2023 16:05:40 +0200 Subject: [PATCH] Assiduites : documentation code python --- app/api/assiduites.py | 280 +++++++++++++++++++++------ app/api/justificatifs.py | 240 ++++++++++++++++------- app/forms/main/config_assiduites.py | 4 +- app/models/assiduites.py | 76 +++++++- app/views/assiduites.py | 286 +++++++++++++++++++--------- tools/downgrade_assiduites.py | 32 +++- 6 files changed, 682 insertions(+), 236 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 33a2413623..ae0993a765 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -163,23 +163,22 @@ def count_assiduites( """ - # query = Identite.query.filter_by(id=etudid) - # if g.scodoc_dept: - # query = query.filter_by(dept_id=g.scodoc_dept_id) - - # etud: Identite = query.first_or_404(etudid) + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) - + # Vérification que l'étudiant existe if etud is None: return json_error( 404, message="étudiant inconnu", ) + # Les filtres qui seront appliqués au comptage (type, date, etudid...) filtered: dict[str, object] = {} + # la métrique du comptage (all, demi, heure, journee) metric: str = "all" + # Si la requête a des paramètres if with_query: metric, filtered = _count_manager(request) @@ -254,11 +253,7 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False) """ - # query = Identite.query.filter_by(id=etudid) - # if g.scodoc_dept: - # query = query.filter_by(dept_id=g.scodoc_dept_id) - - # etud: Identite = query.first_or_404(etudid) + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: @@ -266,15 +261,23 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False) 404, message="étudiant inconnu", ) + # Récupération des assiduités de l'étudiant assiduites_query: Query = etud.assiduites + # Filtrage des assiduités en fonction des paramètres de la requête if with_query: assiduites_query = _filter_manager(request, assiduites_query) + # Préparation de la réponse json + data_set: list[dict] = [] + for ass in assiduites_query.all(): + # conversion Assiduite -> Dict data = ass.to_dict(format_api=True) + # Ajout des justificatifs (ou non dépendamment de la requête) data = _with_justifs(data) + # Ajout de l'assiduité dans la liste de retour data_set.append(data) return data_set @@ -326,6 +329,7 @@ def assiduites_group(with_query: bool = False): """ + # Récupération des étudiants dans la requête etuds = request.args.get("etudids", "") etuds = etuds.split(",") try: @@ -333,6 +337,7 @@ def assiduites_group(with_query: bool = False): except ValueError: return json_error(404, "Le champs etudids n'est pas correctement formé") + # Vérification que tous les étudiants sont du même département query = Identite.query.filter(Identite.id.in_(etuds)) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) @@ -342,15 +347,21 @@ def assiduites_group(with_query: bool = False): 404, "Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.", ) + + # Récupération de toutes les assiduités liés aux étudiants assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds)) + # Filtrage des assiduités en fonction des filtres passés dans la requête if with_query: assiduites_query = _filter_manager(request, assiduites_query) + # Préparation de retour json + # Dict représentant chaque étudiant avec sa liste d'assiduité data_set: dict[list[dict]] = {str(key): [] for key in etuds} for ass in assiduites_query.all(): data = ass.to_dict(format_api=True) data = _with_justifs(data) + # Ajout de l'assiduité dans la liste du bon étudiant data_set.get(str(data["etudid"])).append(data) return data_set @@ -375,20 +386,23 @@ def assiduites_group(with_query: bool = False): @permission_required(Permission.ScoView) def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): """Retourne toutes les assiduités du formsemestre""" + + # Récupération du formsemestre à partir du formsemestre_id formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") + # Récupération des assiduités du formsemestre assiduites_query = scass.filter_by_formsemestre( Assiduite.query, Assiduite, formsemestre ) - + # Filtrage en fonction des paramètres de la requête if with_query: assiduites_query = _filter_manager(request, assiduites_query) + # Préparation du retour JSON data_set: list[dict] = [] for ass in assiduites_query.all(): data = ass.to_dict(format_api=True) @@ -422,21 +436,28 @@ def count_assiduites_formsemestre( formsemestre_id: int = None, with_query: bool = False ): """Comptage des assiduités du formsemestre""" + + # Récupération du formsemestre à partir du formsemestre_id formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() - if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") + # Récupération des étudiants du formsemestre etuds = formsemestre.etuds.all() etuds_id = [etud.id for etud in etuds] + # Récupération des assiduités des étudiants du semestre assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id)) + # Filtrage des assiduités en fonction des dates du semestre assiduites_query = scass.filter_by_formsemestre( assiduites_query, Assiduite, formsemestre ) + + # Gestion de la métrique de comptage (all,demi,heure,journee) metric: str = "all" + # Gestion du filtre (en fonction des paramètres de la requête) filtered: dict = {} if with_query: metric, filtered = _count_manager(request) @@ -481,23 +502,36 @@ def assiduite_create(etudid: int = None, nip=None, ine=None): ] """ + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: return json_error( 404, message="étudiant inconnu", ) + # Mise à jour du "g.scodoc_dept" si route sans dept if g.scodoc_dept is None and etud.dept_id is not None: # route sans département set_sco_dept(etud.departement.acronym) + + # Récupération de la liste des assiduités à créer create_list: list[object] = request.get_json(force=True) + # Vérification que c'est bien une liste if not isinstance(create_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") - errors: list = [] - success: list = [] + # Préparation du retour + + errors: list[dict[str, object]] = [] + success: list[dict[str, object]] = [] + + # Pour chaque objet de la liste, + # on récupère son indice et l'objet for i, data in enumerate(create_list): + # On créé l'assiduité + # 200 + obj si réussi + # 404 + message d'erreur si non réussi code, obj = _create_one(data, etud) if code == 404: errors.append({"indice": i, "message": obj}) @@ -570,53 +604,77 @@ def _create_one( data: dict, etud: Identite, ) -> tuple[int, object]: - """TODO @iziram: documenter""" + """ + _create_one Création d'une assiduité à partir d'une représentation JSON + + Cette fonction vérifie la représentation JSON + + Puis crée l'assiduité si la représentation est valide. + + Args: + data (dict): représentation json d'une assiduité + etud (Identite): l'étudiant concerné par l'assiduité + + Returns: + tuple[int, object]: code, objet + code : 200 si réussi 404 sinon + objet : dict{assiduite_id:?} si réussi str"message d'erreur" sinon + """ + errors: list[str] = [] # -- vérifications de l'objet json -- # cas 1 : ETAT - etat = data.get("etat", None) + etat: str = data.get("etat", None) if etat is None: errors.append("param 'etat': manquant") elif not scu.EtatAssiduite.contains(etat): errors.append("param 'etat': invalide") - etat = scu.EtatAssiduite.get(etat) + etat: scu.EtatAssiduite = scu.EtatAssiduite.get(etat) # cas 2 : date_debut - date_debut = data.get("date_debut", None) + date_debut: str = data.get("date_debut", None) if date_debut is None: errors.append("param 'date_debut': manquant") - deb = scu.is_iso_formated(date_debut, convert=True) + # Conversion de la chaine de caractère en datetime (tz ou non) + deb: datetime = scu.is_iso_formated(date_debut, convert=True) + # si chaine invalide if deb is None: errors.append("param 'date_debut': format invalide") + # Si datetime sans timezone elif deb.tzinfo is None: - deb = scu.localize_datetime(deb) + # Mise à jour de la timezone avec celle du serveur + deb: datetime = scu.localize_datetime(deb) - # cas 3 : date_fin - date_fin = data.get("date_fin", None) + # cas 3 : date_fin (Même fonctionnement ^ ) + date_fin: str = data.get("date_fin", None) if date_fin is None: errors.append("param 'date_fin': manquant") - fin = scu.is_iso_formated(date_fin, convert=True) + fin: datetime = scu.is_iso_formated(date_fin, convert=True) if fin is None: errors.append("param 'date_fin': format invalide") elif fin.tzinfo is None: - fin = scu.localize_datetime(fin) + fin: datetime = scu.localize_datetime(fin) - # cas 5 : desc + # cas 4 : desc desc: str = data.get("desc", None) - external_data = data.get("external_data", None) + # cas 5 : external data + external_data: dict = data.get("external_data", None) if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") - # cas 4 : moduleimpl_id + # cas 6 : moduleimpl_id + # On récupère le moduleimpl moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None + # On vérifie si le moduleimpl existe (uniquement s'il a été renseigné) if moduleimpl_id not in [False, None, "", "-1"]: + # Si le moduleimpl n'est pas "autre" alors on vérifie si l'id est valide if moduleimpl_id != "autre": try: moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() @@ -625,16 +683,23 @@ def _create_one( if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: + # Sinon on met à none le moduleimpl + # et on ajoute dans external data + # le module "autre" moduleimpl_id = None - external_data = external_data if external_data is not None else {} + external_data: dict = external_data if external_data is not None else {} external_data["module"] = "Autre" + # Si il y a des erreurs alors on ne crée pas l'assiduité et on renvoie les erreurs if errors: + # Construit une chaine de caractère avec les erreurs séparées par `,` err: str = ", ".join(errors) + # 404 représente le code d'erreur et err la chaine nouvellement créée return (404, err) - # TOUT EST OK + # SI TOUT EST OK try: + # On essaye de créer l'assiduité nouv_assiduite: Assiduite = Assiduite.create_assiduite( date_debut=deb, date_fin=fin, @@ -647,12 +712,23 @@ def _create_one( notify_mail=True, ) + # create_assiduite générera des ScoValueError si jamais il y a un autre problème + # - Etudiant non inscrit dans le module + # - module obligatoire + # - Assiduité conflictuelles + + # Si tout s'est bien passé on ajoute l'assiduité à la session + # et on retourne un code 200 avec un objet possèdant l'assiduite_id db.session.add(nouv_assiduite) db.session.commit() - return (200, {"assiduite_id": nouv_assiduite.id}) except ScoValueError as excp: # ici on utilise pas json_error car on doit renvoyer status, message + # Ici json_error ne peut être utilisé car il terminerai le processus de création + # Cela voudrait dire qu'une seule erreur dans une assiduité imposerait de + # tout refaire à partir de l'erreur. + + # renvoit un code 404 et le message d'erreur de la ScoValueError return 404, excp.args[0] @@ -675,14 +751,20 @@ def assiduite_delete(): """ + # Récupération des ids envoyés dans la liste assiduites_list: list[int] = request.get_json(force=True) if not isinstance(assiduites_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") + # Préparation du retour json output = {"errors": [], "success": []} - for i, ass in enumerate(assiduites_list): - code, msg = _delete_singular(ass, db) + # Pour chaque assiduite_id on essaye de supprimer l'assiduité + for i, assiduite_id in enumerate(assiduites_list): + # De la même façon que "_create_one" + # Ici le code est soit 200 si réussi ou 404 si raté + # Le message est le message d'erreur si erreur + code, msg = _delete_one(assiduite_id) if code == 404: output["errors"].append({"indice": i, "message": msg}) else: @@ -692,24 +774,43 @@ def assiduite_delete(): return output -def _delete_singular(assiduite_id: int, database) -> tuple[int, str]: - """@iziram PLEASE COMMENT THIS F*CKING CODE""" +def _delete_one(assiduite_id: int) -> tuple[int, str]: + """ + _delete_singular Supprime une assiduité à partir de son id + + Args: + assiduite_id (int): l'identifiant de l'assiduité + Returns: + tuple[int, str]: code, message + code : 200 si réussi, 404 sinon + message : OK si réussi, le message d'erreur sinon + """ assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: - # on ne peut pas utiliser json_error ici car on est déclaré (int, str) + # Ici json_error ne peut être utilisé car il terminerai le processus de création + # Cela voudrait dire qu'une seule erreur d'id imposerait de + # tout refaire à partir de l'erreur. return 404, "Assiduite non existante" + + # Mise à jour du g.scodoc_dept si la route est sans département if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None: # route sans département set_sco_dept(assiduite_unique.etudiant.departement.acronym) - ass_dict = assiduite_unique.to_dict() + + # Récupération de la version dict de l'assiduité + # Pour invalider le cache + assi_dict = assiduite_unique.to_dict() + + # Suppression de l'assiduité et LOG log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( method="delete_assiduite", etudid=assiduite_unique.etudiant.id, msg=f"assiduité: {assiduite_unique}", ) - database.session.delete(assiduite_unique) - scass.simple_invalidate_cache(ass_dict) + db.session.delete(assiduite_unique) + # Invalidation du cache + scass.simple_invalidate_cache(assi_dict) return 200, "OK" @@ -730,17 +831,25 @@ def assiduite_edit(assiduite_id: int): "est_just"?: bool } """ + + # Récupération de l'assiduité à modifier assiduite_unique: Assiduite = Assiduite.query.filter_by( id=assiduite_id ).first_or_404() - errors: list[str] = [] + # Récupération des valeurs à modifier data = request.get_json(force=True) - code, obj = _edit_singular(assiduite_unique, data) + # Préparation du retour + errors: list[str] = [] + + # Code 200 si modification réussie + # Code 404 si raté + message d'erreur + code, obj = _edit_one(assiduite_unique, data) if code == 404: return json_error(404, obj) + # Mise à jour de l'assiduité et LOG log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( "assiduite_edit", @@ -791,7 +900,7 @@ def assiduites_edit(): ) continue - code, obj = _edit_singular(assi, data) + code, obj = _edit_one(assi, data) obj_retour = { "indice": i, "message": obj, @@ -806,46 +915,69 @@ def assiduites_edit(): return {"errors": errors, "success": success} -def _edit_singular(assiduite_unique, data): +def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]: + """ + _edit_singular Modifie une assiduité à partir de données JSON + + Args: + assiduite_unique (Assiduite): l'assiduité à modifier + data (dict): les nouvelles données + + Returns: + tuple[int,str]: code, message + code : 200 si réussi, 404 sinon + message : OK si réussi, message d'erreur sinon + """ + + # Mise à jour du g.scodoc_dept en cas de route sans département if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None: # route sans département set_sco_dept(assiduite_unique.etudiant.departement.acronym) + errors: list[str] = [] # Vérifications de data # Cas 1 : Etat if data.get("etat") is not None: - etat = scu.EtatAssiduite.get(data.get("etat")) + etat: scu.EtatAssiduite = scu.EtatAssiduite.get(data.get("etat")) if etat is None: errors.append("param 'etat': invalide") else: + # Mise à jour de l'état assiduite_unique.etat = etat - external_data = data.get("external_data") + # Cas 2 : external_data + external_data: dict = data.get("external_data") if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") else: + # Mise à jour de l'external data assiduite_unique.external_data = external_data - # Cas 2 : Moduleimpl_id + # Cas 3 : Moduleimpl_id moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None + # False si on modifie pas le moduleimpl if moduleimpl_id is not False: + # Si le module n'est pas nul if moduleimpl_id not in [None, "", "-1"]: + # Gestion du module Autre if moduleimpl_id == "autre": + # module autre = moduleimpl_id:None + external_data["module"]:"Autre" assiduite_unique.moduleimpl_id = None - external_data = ( + external_data: dict = ( external_data if external_data is not None and isinstance(external_data, dict) else assiduite_unique.external_data ) - external_data = external_data if external_data is not None else {} + external_data: dict = external_data if external_data is not None else {} external_data["module"] = "Autre" assiduite_unique.external_data = external_data else: + # Vérification de l'id et récupération de l'objet ModuleImpl try: moduleimpl = ModuleImpl.query.filter_by( id=int(moduleimpl_id) @@ -861,8 +993,11 @@ def _edit_singular(assiduite_unique, data): ): errors.append("param 'moduleimpl_id': etud non inscrit") else: + # Mise à jour du moduleimpl assiduite_unique.moduleimpl_id = moduleimpl_id else: + # Vérification du force module en cas de modification du moduleimpl en moduleimpl nul + # Récupération du formsemestre lié à l'assiduité formsemestre: FormSemestre = get_formsemestre_from_data( assiduite_unique.to_dict() ) @@ -884,12 +1019,12 @@ def _edit_singular(assiduite_unique, data): "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" ) - # Cas 3 : desc - desc = data.get("desc", False) + # Cas 4 : desc + desc: str = data.get("desc", False) if desc is not False: assiduite_unique.description = desc - # Cas 4 : est_just + # Cas 5 : est_just if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: assiduite_unique.est_just = False else: @@ -906,9 +1041,11 @@ def _edit_singular(assiduite_unique, data): ) if errors: + # Retour des erreurs en une seule chaîne séparée par des `,` err: str = ", ".join(errors) return (404, err) + # Mise à jour de l'assiduité et LOG log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}") Scolog.logdb( "assiduite_edit", @@ -1003,21 +1140,34 @@ def _count_manager(requested) -> tuple[str, dict]: def _filter_manager(requested, assiduites_query: Query) -> Query: """ - Retourne les assiduites entrées filtrées en fonction de la request + _filter_manager Retourne les assiduites entrées filtrées en fonction de la request + + Args: + requested (request): La requête http + assiduites_query (Query): la query d'assiduités à filtrer + + Returns: + Query: La query filtrée """ # cas 1 : etat assiduite - etat = requested.args.get("etat") + etat: str = requested.args.get("etat") if etat is not None: assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat) # cas 2 : date de début - deb = requested.args.get("date_debut", "").replace(" ", "+") - deb: datetime = scu.is_iso_formated(deb, True) + deb: str = requested.args.get("date_debut", "").replace(" ", "+") + deb: datetime = scu.is_iso_formated( + deb, True + ) # transformation de la chaine en datetime # cas 3 : date de fin - fin = requested.args.get("date_fin", "").replace(" ", "+") - fin = scu.is_iso_formated(fin, True) + fin: str = requested.args.get("date_fin", "").replace(" ", "+") + fin: datetime = scu.is_iso_formated( + fin, True + ) # transformation de la chaine en datetime + # Pour filtrer les dates il faut forcement avoir les deux bornes + # [date_debut : date_fin] if (deb, fin) != (None, None): assiduites_query: Query = scass.filter_by_date( assiduites_query, Assiduite, deb, fin @@ -1071,10 +1221,12 @@ def _filter_manager(requested, assiduites_query: Query) -> Query: if user_id is not False: assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id) + # cas 9 : order (renvoie la query ordonnée en "date début Décroissante") order = requested.args.get("order", None) if order is not None: assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc()) + # cas 10 : courant (Ne renvoie que les assiduités de l'année courante) courant = requested.args.get("courant", None) if courant is not None: annee: int = scu.annee_scolaire() @@ -1087,7 +1239,17 @@ def _filter_manager(requested, assiduites_query: Query) -> Query: return assiduites_query -def _with_justifs(assi): +def _with_justifs(assi: dict): + """ + _with_justifs ajoute la liste des justificatifs à l'assiduité + + Condition : `with_justifs` doit se trouver dans les paramètres de la requête + Args: + assi (dict): un dictionnaire représentant une assiduité + + Returns: + dict: l'assiduité avec les justificatifs ajoutés + """ if request.args.get("with_justifs") is None: return assi assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index e88933880d..b398b83cbb 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -3,7 +3,7 @@ # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## -"""ScoDoc 9 API : Assiduités +"""ScoDoc 9 API : Justificatifs """ from datetime import datetime @@ -28,6 +28,7 @@ from app.models import ( ) from app.models.assiduites import ( compute_assiduites_justified, + get_formsemestre_from_data, ) from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError @@ -114,7 +115,7 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal query?user_id=[int] ex query?user_id=3 """ - + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: @@ -122,11 +123,15 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal 404, message="étudiant inconnu", ) + + # Récupération des justificatifs de l'étudiant justificatifs_query = etud.justificatifs + # Filtrage des justificatifs en fonction de la requête if with_query: justificatifs_query = _filter_manager(request, justificatifs_query) + # Mise en forme des données puis retour en JSON data_set: list[dict] = [] for just in justificatifs_query.all(): data = just.to_dict(format_api=True) @@ -147,44 +152,51 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal @permission_required(Permission.ScoView) def justificatifs_dept(dept_id: int = None, with_query: bool = False): """ """ - dept = Departement.query.get_or_404(dept_id) - etuds = [etud.id for etud in dept.etudiants] - justificatifs_query = Justificatif.query.filter(Justificatif.etudid.in_(etuds)) + # Récupération du département et des étudiants du département + dept: Departement = Departement.query.get_or_404(dept_id) + etuds: list[int] = [etud.id for etud in dept.etudiants] + # Récupération des justificatifs des étudiants du département + justificatifs_query: Query = Justificatif.query.filter( + Justificatif.etudid.in_(etuds) + ) + + # Filtrage des justificatifs if with_query: - justificatifs_query = _filter_manager(request, justificatifs_query) + justificatifs_query: Query = _filter_manager(request, justificatifs_query) + # Mise en forme des données et retour JSON data_set: list[dict] = [] for just in justificatifs_query: - data_set.append(_set_sems_and_groupe(just)) + data_set.append(_set_sems(just)) return data_set -def _set_sems_and_groupe(justi: Justificatif) -> dict: - from app.scodoc.sco_groups import get_etud_groups +def _set_sems(justi: Justificatif) -> dict: + """ + _set_sems Ajoute le formsemestre associé au justificatif s'il existe + Si le formsemestre n'existe pas, renvoie la simple représentation du justificatif + + Args: + justi (Justificatif): Le justificatif + + Returns: + dict: La représentation de l'assiduité en dictionnaire + """ + # Conversion du justificatif en dictionnaire data = justi.to_dict(format_api=True) - formsemestre: FormSemestre = ( - FormSemestre.query.join( - FormSemestreInscription, - FormSemestre.id == FormSemestreInscription.formsemestre_id, - ) - .filter( - justi.date_debut <= FormSemestre.date_fin, - justi.date_fin >= FormSemestre.date_debut, - FormSemestreInscription.etudid == justi.etudid, - ) - .first() - ) + # Récupération du formsemestre de l'assiduité + formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict()) + # Si le formsemestre existe on l'ajoute au dictionnaire if formsemestre: data["formsemestre"] = { "id": formsemestre.id, "title": formsemestre.session_id(), } - return data @@ -208,20 +220,27 @@ def _set_sems_and_groupe(justi: Justificatif) -> dict: @permission_required(Permission.ScoView) def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): """Retourne tous les justificatifs du formsemestre""" + + # Récupération du formsemestre formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) - formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id + ).first() if formsemestre is None: return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") - justificatifs_query = scass.filter_by_formsemestre( + # Récupération des justificatifs du semestre + justificatifs_query: Query = scass.filter_by_formsemestre( Justificatif.query, Justificatif, formsemestre ) + # Filtrage des justificatifs if with_query: - justificatifs_query = _filter_manager(request, justificatifs_query) + justificatifs_query: Query = _filter_manager(request, justificatifs_query) + # Retour des justificatifs en JSON data_set: list[dict] = [] for justi in justificatifs_query.all(): data = justi.to_dict(format_api=True) @@ -264,6 +283,8 @@ def justif_create(etudid: int = None, nip=None, ine=None): ] """ + + # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) if etud is None: @@ -272,16 +293,22 @@ def justif_create(etudid: int = None, nip=None, ine=None): message="étudiant inconnu", ) + # Récupération des justificatifs à créer create_list: list[object] = request.get_json(force=True) if not isinstance(create_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") - errors: list = [] - success: list = [] - justifs: list = [] + errors: list[dict] = [] + success: list[dict] = [] + justifs: list[Justificatif] = [] + + # énumération des justificatifs for i, data in enumerate(create_list): code, obj, justi = _create_one(data, etud) + code: int + obj: str | dict + justi: Justificatif | None if code == 404: errors.append({"indice": i, "message": obj}) else: @@ -289,6 +316,7 @@ def justif_create(etudid: int = None, nip=None, ine=None): justifs.append(justi) 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} @@ -296,32 +324,32 @@ def justif_create(etudid: int = None, nip=None, ine=None): def _create_one( data: dict, etud: Identite, -) -> tuple[int, object]: +) -> tuple[int, object, Justificatif]: errors: list[str] = [] # -- vérifications de l'objet json -- # cas 1 : ETAT - etat = data.get("etat", None) + etat: str = data.get("etat", None) if etat is None: errors.append("param 'etat': manquant") elif not scu.EtatJustificatif.contains(etat): errors.append("param 'etat': invalide") - etat = scu.EtatJustificatif.get(etat) + etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat) # cas 2 : date_debut - date_debut = data.get("date_debut", None) + date_debut: str = data.get("date_debut", None) if date_debut is None: errors.append("param 'date_debut': manquant") - deb = scu.is_iso_formated(date_debut, convert=True) + deb: datetime = scu.is_iso_formated(date_debut, convert=True) if deb is None: errors.append("param 'date_debut': format invalide") # cas 3 : date_fin - date_fin = data.get("date_fin", None) + date_fin: str = data.get("date_fin", None) if date_fin is None: errors.append("param 'date_fin': manquant") - fin = scu.is_iso_formated(date_fin, convert=True) + fin: datetime = scu.is_iso_formated(date_fin, convert=True) if fin is None: errors.append("param 'date_fin': format invalide") @@ -329,7 +357,7 @@ def _create_one( raison: str = data.get("raison", None) - external_data = data.get("external_data") + external_data: dict = data.get("external_data") if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") @@ -341,6 +369,7 @@ def _create_one( # TOUT EST OK try: + # On essaye de créer le justificatif nouv_justificatif: Query = Justificatif.create_justificatif( date_debut=deb, date_fin=fin, @@ -351,6 +380,11 @@ def _create_one( external_data=external_data, ) + # Si tout s'est bien passé on ajoute l'assiduité à la session + # et on retourne un code 200 avec un objet possèdant le justif_id + # ainsi que les assiduités justifiées par le dit justificatif + + # On renvoie aussi le justificatif créé pour pour le calcul total de fin db.session.add(nouv_justificatif) db.session.commit() @@ -363,10 +397,7 @@ def _create_one( nouv_justificatif, ) except ScoValueError as excp: - return ( - 404, - excp.args[0], - ) + return (404, excp.args[0], None) @bp.route("/justificatif//edit", methods=["POST"]) @@ -387,53 +418,58 @@ def justif_edit(justif_id: int): "date_fin"?: str } """ + + # Récupération du justificatif à modifier justificatif_unique: Query = Justificatif.query.filter_by( id=justif_id ).first_or_404() errors: list[str] = [] data = request.get_json(force=True) + + # Récupération des assiduités (id) précédemment justifiée par le justificatif avant_ids: list[int] = scass.justifies(justificatif_unique) # Vérifications de data # Cas 1 : Etat if data.get("etat") is not None: - etat = scu.EtatJustificatif.get(data.get("etat")) + etat: scu.EtatJustificatif = scu.EtatJustificatif.get(data.get("etat")) if etat is None: errors.append("param 'etat': invalide") else: justificatif_unique.etat = etat # Cas 2 : raison - raison = data.get("raison", False) + raison: str = data.get("raison", False) if raison is not False: justificatif_unique.raison = raison deb, fin = None, None # cas 3 : date_debut - date_debut = data.get("date_debut", False) + date_debut: str = data.get("date_debut", False) if date_debut is not False: if date_debut is None: errors.append("param 'date_debut': manquant") - deb = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True) + deb: datetime = scu.is_iso_formated(date_debut.replace(" ", "+"), convert=True) if deb is None: errors.append("param 'date_debut': format invalide") # cas 4 : date_fin - date_fin = data.get("date_fin", False) + date_fin: str = data.get("date_fin", False) if date_fin is not False: if date_fin is None: errors.append("param 'date_fin': manquant") - fin = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True) + fin: datetime = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True) if fin is None: errors.append("param 'date_fin': format invalide") - # Mise à jour des dates + # Récupération des dates précédentes si deb ou fin est None deb = deb if deb is not None else justificatif_unique.date_debut fin = fin if fin is not None else justificatif_unique.date_fin - external_data = data.get("external_data") + # Mise à jour de l'external data + external_data: dict = data.get("external_data") if external_data is not None: if not isinstance(external_data, dict): errors.append("param 'external_data' : n'est pas un objet JSON") @@ -443,6 +479,7 @@ def justif_edit(justif_id: int): if fin <= deb: errors.append("param 'dates' : Date de début après date de fin") + # Mise à jour des dates du justificatif justificatif_unique.date_debut = deb justificatif_unique.date_fin = fin @@ -450,9 +487,14 @@ def justif_edit(justif_id: int): err: str = ", ".join(errors) return json_error(404, err) + # Mise à jour du justificatif db.session.add(justificatif_unique) db.session.commit() + # Génération du dictionnaire de retour + # La couverture correspond + # - aux assiduités précédemment justifiées par le justificatif + # - aux assiduités qui sont justifiées par le justificatif modifié retour = { "couverture": { "avant": avant_ids, @@ -463,7 +505,7 @@ def justif_edit(justif_id: int): ), } } - + # Invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) return retour @@ -487,6 +529,8 @@ def justif_delete(): """ + + # Récupération des justif_ids justificatifs_list: list[int] = request.get_json(force=True) if not isinstance(justificatifs_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") @@ -494,7 +538,7 @@ def justif_delete(): output = {"errors": [], "success": []} for i, ass in enumerate(justificatifs_list): - code, msg = _delete_singular(ass, db) + code, msg = _delete_one(ass) if code == 404: output["errors"].append({"indice": i, "message": msg}) else: @@ -505,22 +549,41 @@ def justif_delete(): return output -def _delete_singular(justif_id: int, database): - justificatif_unique: Query = Justificatif.query.filter_by(id=justif_id).first() +def _delete_one(justif_id: int) -> tuple[int, str]: + """ + _delete_one Supprime un justificatif + + Args: + justif_id (int): l'identifiant du justificatif + + Returns: + tuple[int, str]: code, message + code : 200 si réussi, 404 sinon + message : OK si réussi, message d'erreur sinon + """ + # Récupération du justificatif à supprimer + justificatif_unique: Justificatif = Justificatif.query.filter_by( + id=justif_id + ).first() if justificatif_unique is None: return (404, "Justificatif non existant") + # Récupération de l'archive du justificatif archive_name: str = justificatif_unique.fichier if archive_name is not None: + # Si elle existe : on essaye de la supprimer archiver: JustificatifArchiver = JustificatifArchiver() try: archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) except ValueError: pass + # On invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) - database.session.delete(justificatif_unique) + # 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(), @@ -541,23 +604,27 @@ def justif_import(justif_id: int = None): """ Importation d'un fichier (création d'archive) """ + + # On vérifie qu'un fichier a bien été envoyé if len(request.files) == 0: return json_error(404, "Il n'y a pas de fichier joint") - file = list(request.files.values())[0] if file.filename == "": return json_error(404, "Il n'y a pas de fichier joint") + # On récupère le justificatif auquel on va importer le fichier query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # Récupération de l'archive si elle existe archive_name: str = justificatif_unique.fichier + # Utilisation de l'archiver de justificatifs archiver: JustificatifArchiver = JustificatifArchiver() try: + # On essaye de sauvegarder le fichier fname: str archive_name, fname = archiver.save_justificatif( justificatif_unique.etudiant, @@ -567,6 +634,7 @@ def justif_import(justif_id: int = None): user_id=current_user.id, ) + # On actualise l'archive du justificatif justificatif_unique.fichier = archive_name db.session.add(justificatif_unique) @@ -574,6 +642,7 @@ def justif_import(justif_id: int = None): return {"filename": fname} except ScoValueError as err: + # Si cela ne fonctionne pas on renvoie une erreur return json_error(404, err.args[0]) @@ -587,23 +656,26 @@ def justif_export(justif_id: int = None, filename: str = None): Retourne un fichier d'une archive d'un justificatif """ + # On récupère le justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # On récupère l'archive concernée archive_name: str = justificatif_unique.fichier if archive_name is None: + # On retourne une erreur si le justificatif n'a pas de fichiers return json_error(404, "le justificatif ne possède pas de fichier") + # On récupère le fichier et le renvoie en une réponse déjà formée archiver: JustificatifArchiver = JustificatifArchiver() - try: return archiver.get_justificatif_file( archive_name, justificatif_unique.etudiant, filename ) except ScoValueError as err: + # On retourne une erreur json si jamais il y a un problème return json_error(404, err.args[0]) @@ -616,7 +688,6 @@ def justif_export(justif_id: int = None, filename: str = None): def justif_remove(justif_id: int = None): """ Supression d'un fichier ou d'une archive - # TOTALK: Doc, expliquer les noms coté server { "remove": <"all"/"list"> @@ -627,31 +698,41 @@ def justif_remove(justif_id: int = None): } """ + # On récupère le dictionnaire data: dict = request.get_json(force=True) + # On récupère le justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # On récupère l'archive archive_name: str = justificatif_unique.fichier if archive_name is None: + # On retourne une erreur si le justificatif n'a pas de fichiers return json_error(404, "le justificatif ne possède pas de fichier") + # On regarde le type de suppression (all ou list) + # Si all : on supprime tous les fichiers + # Si list : on supprime les fichiers dont le nom est dans la liste remove: str = data.get("remove") if remove is None or remove not in ("all", "list"): return json_error(404, "param 'remove': Valeur invalide") + + # On récupère l'archiver et l'étudiant archiver: JustificatifArchiver = JustificatifArchiver() etud = justificatif_unique.etudiant try: if remove == "all": + # Suppression de toute l'archive du justificatif archiver.delete_justificatif(etud, archive_name=archive_name) justificatif_unique.fichier = None db.session.add(justificatif_unique) db.session.commit() else: + # Suppression des fichiers dont le nom se trouve dans la liste "filenames" for fname in data.get("filenames", []): archiver.delete_justificatif( etud, @@ -659,6 +740,7 @@ def justif_remove(justif_id: int = None): filename=fname, ) + # Si il n'y a plus de fichiers dans l'archive, on la supprime if len(archiver.list_justificatifs(archive_name, etud)) == 0: archiver.delete_justificatif(etud, archive_name) justificatif_unique.fichier = None @@ -666,8 +748,10 @@ def justif_remove(justif_id: int = None): db.session.commit() except ScoValueError as err: + # On retourne une erreur json si jamais il y a eu un problème return json_error(404, err.args[0]) + # On retourne une réponse "removed" si tout s'est bien passé return {"response": "removed"} @@ -682,29 +766,36 @@ def justif_list(justif_id: int = None): Liste les fichiers du justificatif """ + # Récupération du justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # Récupération de l'archive avec l'archiver archive_name: str = justificatif_unique.fichier - filenames: list[str] = [] - archiver: JustificatifArchiver = JustificatifArchiver() if archive_name is not None: filenames = archiver.list_justificatifs( archive_name, justificatif_unique.etudiant ) - + # Préparation du retour + # - total : le nombre total de fichier du justificatif + # - filenames : le nom des fichiers visible par l'utilisateur retour = {"total": len(filenames), "filenames": []} + # Pour chaque nom de fichier on vérifie + # - Si l'utilisateur qui a importé le fichier est le même que + # l'utilisateur qui a demandé la liste des fichiers + # - Ou si l'utilisateur qui a demandé la liste possède la permission AbsJustifView + # Si c'est le cas alors on ajoute à la liste des fichiers visibles for filename in filenames: if int(filename[1]) == current_user.id or current_user.has_permission( Permission.AbsJustifView ): retour["filenames"].append(filename[0]) + # On renvoie le total et la liste des fichiers visibles return retour @@ -720,44 +811,45 @@ def justif_justifies(justif_id: int = None): Liste assiduite_id justifiées par le justificatif """ + # On récupère le justificatif concerné query: Query = Justificatif.query.filter_by(id=justif_id) if g.scodoc_dept: query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique: Justificatif = query.first_or_404() + # On récupère la liste des assiduités justifiées par le justificatif assiduites_list: list[int] = scass.justifies(justificatif_unique) - + # On la renvoie return assiduites_list # -- Utils -- -def _filter_manager(requested, justificatifs_query): +def _filter_manager(requested, justificatifs_query: Query): """ Retourne les justificatifs entrés filtrés en fonction de la request """ # cas 1 : etat justificatif - etat = requested.args.get("etat") + etat: str = requested.args.get("etat") if etat is not None: - justificatifs_query = scass.filter_justificatifs_by_etat( + justificatifs_query: Query = scass.filter_justificatifs_by_etat( justificatifs_query, etat ) # cas 2 : date de début - deb = requested.args.get("date_debut", "").replace(" ", "+") + deb: str = requested.args.get("date_debut", "").replace(" ", "+") deb: datetime = scu.is_iso_formated(deb, True) # cas 3 : date de fin - fin = requested.args.get("date_fin", "").replace(" ", "+") - fin = scu.is_iso_formated(fin, True) + fin: str = requested.args.get("date_fin", "").replace(" ", "+") + fin: datetime = scu.is_iso_formated(fin, True) if (deb, fin) != (None, None): justificatifs_query: Query = scass.filter_by_date( justificatifs_query, Justificatif, deb, fin ) - + # cas 4 : user_id user_id = requested.args.get("user_id", False) if user_id is not False: justificatifs_query: Query = scass.filter_by_user_id( @@ -778,12 +870,13 @@ def _filter_manager(requested, justificatifs_query): except ValueError: formsemestre = None + # cas 6 : order (retourne les justificatifs par ordre décroissant de date_debut) order = requested.args.get("order", None) if order is not None: justificatifs_query: Query = justificatifs_query.order_by( Justificatif.date_debut.desc() ) - + # cas 7 : courant (retourne uniquement les justificatifs de l'année scolaire courante) courant = requested.args.get("courant", None) if courant is not None: annee: int = scu.annee_scolaire() @@ -793,6 +886,7 @@ def _filter_manager(requested, justificatifs_query): Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee), ) + # cas 8 : group_id filtre les justificatifs d'un groupe d'étudiant group_id = requested.args.get("group_id", None) if group_id is not None: try: diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py index 7f2cdaa20a..ba7198b823 100644 --- a/app/forms/main/config_assiduites.py +++ b/app/forms/main/config_assiduites.py @@ -37,7 +37,9 @@ import datetime class TimeField(StringField): - """HTML5 time input.""" + """HTML5 time input. + tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f + """ widget = TimeInput() diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 57b7dfb772..a89e89b4f6 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -82,6 +82,7 @@ class Assiduite(db.Model): etat = self.etat user: User = None if format_api: + # format api utilise les noms "present,absent,retard" au lieu des int etat = EtatAssiduite.inverse().get(self.etat).name if self.user_id is not None: user = db.session.get(User, self.user_id) @@ -345,15 +346,10 @@ def is_period_conflicting( avec les justificatifs ou assiduites déjà présentes """ + # On s'assure que les dates soient avec TimeZone date_debut = localize_datetime(date_debut) date_fin = localize_datetime(date_fin) - if ( - collection.filter_by(date_debut=date_debut, date_fin=date_fin).first() - is not None - ): - return True - count: int = collection.filter( collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut ).count() @@ -375,19 +371,26 @@ def compute_assiduites_justified( Returns: list[int]: la liste des assiduités qui ont été justifiées. """ + # Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant if justificatifs is None: - justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() + 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, @@ -395,21 +398,39 @@ def compute_assiduites_justified( 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): +def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]: + """ + get_assiduites_justif Récupération des justificatifs d'une assiduité + + Args: + assiduite_id (int): l'identifiant de l'assiduité + long (bool): Retourner des dictionnaires à la place + des identifiants des justificatifs + + Returns: + list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai) + """ assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long) @@ -420,20 +441,57 @@ def get_justifs_from_date( date_fin: datetime, long: bool = False, valid: bool = False, -): +) -> list[int | dict]: + """ + get_justifs_from_date Récupération des justificatifs couvrant une période pour un étudiant donné + + Args: + etudid (int): l'identifiant de l'étudiant + date_debut (datetime): la date de début (datetime avec timezone) + date_fin (datetime): la date de fin (datetime avec timezone) + long (bool, optional): Définition de la sortie. + Vrai pour avoir les dictionnaires des justificatifs. + Faux pour avoir uniquement les identifiants + Defaults to False. + valid (bool, optional): Filtre pour n'avoir que les justificatifs valide. + Si vrai : le retour ne contiendra que des justificatifs valides + Sinon le retour contiendra tout type de justificatifs + Defaults to False. + + Returns: + list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai) + """ + # On récupère les justificatifs d'un étudiant couvrant la période donnée justifs: Query = Justificatif.query.filter( Justificatif.etudid == etudid, Justificatif.date_debut <= date_debut, Justificatif.date_fin >= date_fin, ) + # si valide est vrai alors on filtre pour n'avoir que les justificatifs valide if valid: justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE) + # On renvoie la liste des id des justificatifs si long est Faux, sinon on renvoie les dicts des justificatifs return [j.justif_id if not long else j.to_dict(True) for j in justifs] def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre: + """ + get_formsemestre_from_data récupère un formsemestre en fonction des données passées + + Args: + data (dict[str, datetime | int]): Une réprésentation simplifiée d'une assiduité ou d'un justificatif + + data = { + "etudid" : int, + "date_debut": datetime (tz), + "date_fin": datetime (tz), + } + + Returns: + FormSemestre: Le formsemestre trouvé ou None + """ return ( FormSemestre.query.join( FormSemestreInscription, diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 2033476a04..da56a2e692 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -160,6 +160,8 @@ class HTMLBuilder: @permission_required(Permission.AbsChange) def bilan_dept(): """Gestionnaire assiduités, page principale""" + + # Préparation de la page H = [ html_sco_header.sco_header( page_title="Saisie de l'assiduité", @@ -183,7 +185,10 @@ def bilan_dept(): """

Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant, choisissez d'abord la personne concernée :

""" ) + # Ajout de la barre de recherche d'étudiant (redirection vers bilan etud) H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud")) + + # Gestion des billets d'absences if current_user.has_permission( Permission.AbsChange ) and sco_preferences.get_preference("handle_billets_abs"): @@ -195,19 +200,23 @@ def bilan_dept(): """ ) + + # Récupération des années d'étude du département + # (afin de sélectionner une année) dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first() annees: list[int] = sorted( [f.date_debut.year for f in dept.formsemestres], reverse=True, ) - - annee = scu.annee_scolaire() - + annee = scu.annee_scolaire() # Année courante, sera utilisée par défaut + # Génération d'une liste "json" d'années annees_str: str = "[" for ann in annees: annees_str += f"{ann}," annees_str += "]" + # Récupération d'un formsemestre + # (pour n'afficher que les assiduites/justificatifs liés au formsemestre) formsemestre_id = request.args.get("formsemestre_id", "") if formsemestre_id: try: @@ -216,6 +225,7 @@ def bilan_dept(): except AttributeError: formsemestre_id = "" + # Peuplement du template jinja H.append( render_template( "assiduites/pages/bilan_dept.j2", @@ -230,49 +240,6 @@ def bilan_dept(): return "\n".join(H) -# @bp.route("/ListeSemestre") -# @scodoc -# @permission_required(Permission.ScoView) -# def liste_assiduites_formsemestre(): -# """ -# liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant -# Args: -# etudid (int): l'identifiant de l'étudiant - -# Returns: -# str: l'html généré -# """ - -# formsemestre_id = request.args.get("formsemestre_id", -1) -# formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) -# if formsemestre.dept_id != g.scodoc_dept_id: -# abort(404, "FormSemestre inexistant dans ce département") - -# header: str = html_sco_header.sco_header( -# page_title="Liste des assiduités du semestre", -# init_qtip=True, -# javascripts=[ -# "js/assiduites.js", -# "libjs/moment-2.29.4.min.js", -# "libjs/moment-timezone.js", -# ], -# cssstyles=CSSSTYLES -# + [ -# "css/assiduites.css", -# ], -# ) - -# return HTMLBuilder( -# header, -# render_template( -# "assiduites/pages/liste_semestre.j2", -# sco=ScoData(formsemestre=formsemestre), -# sem=formsemestre.titre_annee(), -# formsemestre_id=formsemestre.id, -# ), -# ).build() - - @bp.route("/SignaleAssiduiteEtud") @scodoc @permission_required(Permission.AbsChange) @@ -287,22 +254,22 @@ def signal_assiduites_etud(): str: l'html généré """ + # Récupération de l'étudiant concerné etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Récupération de la date (par défaut la date du jour) date = request.args.get("date", datetime.date.today().isoformat()) - # gestion évaluations + # gestion évaluations (Appel à la page depuis les évaluations) saisie_eval: bool = request.args.get("saisie_eval") is not None - date_deb: str = request.args.get("date_deb") date_fin: str = request.args.get("date_fin") moduleimpl_id: int = request.args.get("moduleimpl_id", "") evaluation_id: int = request.args.get("evaluation_id") - redirect_url: str = ( "#" if not saisie_eval @@ -313,6 +280,7 @@ def signal_assiduites_etud(): ) ) + # Préparation de la page (Header) header: str = html_sco_header.sco_header( page_title="Saisie assiduité", init_qtip=True, @@ -335,11 +303,14 @@ def signal_assiduites_etud(): "assi_afternoon_time", "18:00:00" ) + # Gestion du selecteur de moduleimpl (pour le tableau différé) select = f""" """ + + # Génération de la page return HTMLBuilder( header, _mini_timeline(), @@ -381,13 +352,16 @@ def liste_assiduites_etud(): str: l'html généré """ + # Récupération de l'étudiant concerné etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Gestion d'une assiduité unique (redirigé depuis le calendrier) assiduite_id: int = request.args.get("assiduite_id", -1) + # Préparation de la page header: str = html_sco_header.sco_header( page_title=f"Assiduité de {etud.nomprenom}", init_qtip=True, @@ -401,7 +375,7 @@ def liste_assiduites_etud(): "css/assiduites.css", ], ) - + # Peuplement du template jinja return HTMLBuilder( header, render_template( @@ -429,12 +403,13 @@ def bilan_etud(): Returns: str: l'html généré """ - + # Récupération de l'étudiant etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Préparation de la page (header) header: str = html_sco_header.sco_header( page_title=f"Bilan de l'assiduité de {etud.nomprenom}", init_qtip=True, @@ -449,13 +424,16 @@ def bilan_etud(): ], ) + # Gestion des dates du bilan (par défaut l'année scolaire) date_debut: str = f"{scu.annee_scolaire()}-09-01" date_fin: str = f"{scu.annee_scolaire()+1}-06-30" + # Récupération de la métrique d'assiduité assi_metric = scu.translate_assiduites_metric( sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id), ) + # Génération de la page return HTMLBuilder( header, render_template( @@ -486,11 +464,13 @@ def ajout_justificatif_etud(): str: l'html généré """ + # Récupération de l'étudiant concerné etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Préparation de la page (header) header: str = html_sco_header.sco_header( page_title="Justificatifs", init_qtip=True, @@ -505,6 +485,7 @@ def ajout_justificatif_etud(): ], ) + # Peuplement du template jinja return HTMLBuilder( header, render_template( @@ -533,11 +514,13 @@ def calendrier_etud(): str: l'html généré """ + # Récupération de l'étudiant etudid = request.args.get("etudid", -1) etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Préparation de la page header: str = html_sco_header.sco_header( page_title="Calendrier de l'assiduité", init_qtip=True, @@ -552,16 +535,20 @@ def calendrier_etud(): ], ) + # Récupération des années d'étude de l'étudiant annees: list[int] = sorted( [ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions], reverse=True, ) + # Transformation en une liste "json" + # (sera utilisé pour générer le selecteur d'année) annees_str: str = "[" for ann in annees: annees_str += f"{ann}," annees_str += "]" + # Peuplement du template jinja return HTMLBuilder( header, render_template( @@ -585,11 +572,11 @@ def signal_assiduites_group(): Returns: str: l'html généré """ + # Récupération des paramètres de l'url formsemestre_id: int = request.args.get("formsemestre_id", -1) moduleimpl_id: int = request.args.get("moduleimpl_id") date: str = request.args.get("jour", datetime.date.today().isoformat()) group_ids: list[int] = request.args.get("group_ids", None) - if group_ids is None: group_ids = [] else: @@ -601,12 +588,14 @@ def signal_assiduites_group(): moduleimpl_id = int(moduleimpl_id) except (TypeError, ValueError): moduleimpl_id = None + # Vérification du formsemestre_id try: formsemestre_id = int(formsemestre_id) except (TypeError, ValueError): formsemestre_id = None + # Gestion des groupes groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id ) @@ -617,10 +606,6 @@ def signal_assiduites_group(): + html_sco_header.sco_footer() ) - # --- URL DEFAULT --- - - base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}" - # --- Filtrage par formsemestre --- formsemestre_id = groups_infos.formsemestre_id @@ -628,17 +613,22 @@ def signal_assiduites_group(): if formsemestre.dept_id != g.scodoc_dept_id: abort(404, "groupes inexistants dans ce département") + # Vérification du forçage du module require_module = sco_preferences.get_preference("forcer_module", formsemestre_id) + + # Récupération des étudiants des groupes etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] # --- Vérification de la date --- - real_date = scu.is_iso_formated(date, True).date() if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin: + # Si le jour est hors semestre, indiquer une erreur + + # Formatage des dates pour le message d'erreur real_str = real_date.strftime("%d/%m/%Y") form_deb = formsemestre.date_debut.strftime("%d/%m/%Y") form_fin = formsemestre.date_fin.strftime("%d/%m/%Y") @@ -662,8 +652,7 @@ def signal_assiduites_group(): # Si aucun etudiant n'est inscrit au module choisi... moduleimpl_id = None - # --- Génération de l'HTML --- - sem = formsemestre.to_dict() + # Récupération du nom des/du groupe(s) if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" @@ -676,12 +665,15 @@ def signal_assiduites_group(): grp + ' ' + groups_infos.groups_titles + "" ) + # --- Génération de l'HTML --- + header: str = html_sco_header.sco_header( page_title="Saisie journalière des assiduités", init_qtip=True, javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ # Voir fonctionnement JS + # XXX Retirer moment "js/etud_info.js", "js/groups_view.js", "js/assiduites.js", @@ -694,6 +686,10 @@ def signal_assiduites_group(): ], ) + # Récupération du semestre en dictionnaire + sem = formsemestre.to_dict() + + # Peuplement du template jinja return HTMLBuilder( header, _mini_timeline(), @@ -731,11 +727,12 @@ def visu_assiduites_group(): Returns: str: l'html généré """ + + # Récupération des paramètres de la requête formsemestre_id: int = request.args.get("formsemestre_id", -1) moduleimpl_id: int = request.args.get("moduleimpl_id") date: str = request.args.get("jour", datetime.date.today().isoformat()) group_ids: list[int] = request.args.get("group_ids", None) - if group_ids is None: group_ids = [] else: @@ -755,6 +752,7 @@ def visu_assiduites_group(): except (TypeError, ValueError) as exc: raise ScoValueError("identifiant de formsemestre invalide") from exc + # Récupérations des/du groupe(s) groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id ) @@ -765,10 +763,6 @@ def visu_assiduites_group(): + html_sco_header.sco_footer() ) - # --- URL DEFAULT --- - - base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}" - # --- Filtrage par formsemestre --- formsemestre_id = groups_infos.formsemestre_id @@ -776,10 +770,10 @@ def visu_assiduites_group(): if formsemestre.dept_id != g.scodoc_dept_id: abort(404, "groupes inexistants dans ce département") - require_module = sco_preferences.get_preference( - "abs_require_module", formsemestre_id - ) + # Vérfication du forçage du module + require_module = sco_preferences.get_preference("forcer_module", formsemestre_id) + # Récupération des étudiants du/des groupe(s) etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members @@ -810,7 +804,6 @@ def visu_assiduites_group(): moduleimpl_id = None # --- Génération de l'HTML --- - sem = formsemestre.to_dict() if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" @@ -841,6 +834,9 @@ def visu_assiduites_group(): ], ) + # Récupération du semestre en dictionnaire + sem = formsemestre.to_dict() + return HTMLBuilder( header, _mini_timeline(), @@ -873,10 +869,14 @@ def visu_assiduites_group(): @permission_required(Permission.ScoView) def etat_abs_date(): """date_debut, date_fin en ISO""" + + # Récupération des paramètre de la requête date_debut_str = request.args.get("date_debut") date_fin_str = request.args.get("date_fin") title = request.args.get("desc") group_ids: list[int] = request.args.get("group_ids", None) + + # Vérification des dates try: date_debut = datetime.datetime.fromisoformat(date_debut_str) except ValueError as exc: @@ -885,6 +885,8 @@ def etat_abs_date(): date_fin = datetime.datetime.fromisoformat(date_fin_str) except ValueError as exc: raise ScoValueError("date_fin invalide") from exc + + # Vérification des groupes if group_ids is None: group_ids = [] else: @@ -893,25 +895,30 @@ def etat_abs_date(): groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) + # Récupération des étudiants des groupes etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] + # Récupération des assiduites des étudiants assiduites: Assiduite = Assiduite.query.filter( Assiduite.etudid.in_([e["etudid"] for e in etuds]) ) + # Filtrage des assiduités en fonction des dates données assiduites = scass.filter_by_date( assiduites, Assiduite, date_debut, date_fin, False ) + # Génération d'objet étudiant simplifié (nom+lien cal, etat_assiduite) etudiants: list[dict] = [] for etud in etuds: + # On récupère l'état de la première assiduité sur la période assi = assiduites.filter_by(etudid=etud["etudid"]).first() - etat = "" if assi is not None and assi.etat != 0: etat = scu.EtatAssiduite.inverse().get(assi.etat).name + # On génère l'objet simplifié etudiant = { "nom": f"""' + groups_infos.groups_titles + "" ) + # Génération de la page return render_template( "assiduites/pages/visu_assi.j2", assi_metric=scu.translate_assiduites_metric( @@ -1013,26 +1030,27 @@ def visu_assi_group(): @scodoc @permission_required(Permission.AbsChange) def signal_assiduites_diff(): + # 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) date: str = request.args.get("jour", datetime.date.today().isoformat()) - date_deb: str = request.args.get("date_deb") date_fin: str = request.args.get("date_fin") - semaine: str = request.args.get("semaine") + # Dans le cas où on donne une semaine plutot qu'un jour if semaine is not None: + # On génère la semaine iso à partir de l'anne scolaire. semaine = ( f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine ) + # On met à jour les dates avec le date de debut et fin de semaine date_deb: datetime.date = datetime.datetime.strptime( semaine + "-1", "%Y-W%W-%w" ) date_fin: datetime.date = date_deb + datetime.timedelta(days=6) etudiants: list[dict] = [] - titre = None # Vérification du formsemestre_id @@ -1052,14 +1070,13 @@ def signal_assiduites_diff(): elif real_date > formsemestre.date_fin: date = formsemestre.date_fin.isoformat() + # 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) - if not groups_infos.members: return ( html_sco_header.sco_header(page_title="Assiduité: saisie différée") @@ -1067,6 +1084,7 @@ def signal_assiduites_diff(): + html_sco_header.sco_footer() ) + # Récupération des étudiants etudiants.extend( [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] @@ -1076,6 +1094,8 @@ def signal_assiduites_diff(): etudiants = list(sorted(etudiants, key=lambda x: x["nom"])) + # Génération de l'HTML + header: str = html_sco_header.sco_header( page_title="Assiduité: saisie différée", init_qtip=True, @@ -1142,13 +1162,17 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): Alors l'absence sera sur la période de l'évaluation Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant """ + + # Récupération de l'étudiant concerné etud: Identite = Identite.query.get_or_404(etudid) if etud.dept_id != g.scodoc_dept_id: abort(404, "étudiant inexistant dans ce département") + # Récupération de l'évaluation concernée evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut + # Si l'évaluation dure plus qu'un jour alors on redirige vers la page de saisie etudiant if delta > datetime.timedelta(days=1): # rediriger vers page saisie return redirect( @@ -1164,7 +1188,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): ) ) - # créer l'assiduité + # Sinon on créé l'assiduité try: assiduite_unique: Assiduite = Assiduite.create_assiduite( @@ -1175,8 +1199,8 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): moduleimpl=evaluation.moduleimpl, ) except ScoValueError as see: + # En cas d'erreur msg: str = see.args[0] - if "Duplication" in msg: msg = "Une autre assiduité concerne déjà cette période. En cliquant sur continuer vous serez redirigé vers la page de saisie des assiduités de l'étudiant." dest: str = url_for( @@ -1195,6 +1219,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): db.session.add(assiduite_unique) db.session.commit() + # on flash pour indiquer que l'absence a bien été créée puis on revient sur la page de l'évaluation flash("L'absence a bien été créée") # rediriger vers la page d'évaluation return redirect( @@ -1208,16 +1233,20 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: """Génère la liste des assiduités d'un étudiant pour le bulletin mail""" + + # On récupère la métrique d'assiduité metrique: str = scu.translate_assiduites_metric( sco_preferences.get_preference("assi_metrique", formsemestre_id=semestre.id), ) + # On récupère le nombre maximum de ligne d'assiduité max_nb: int = int( sco_preferences.get_preference( "bul_mail_list_abs_nb", formsemestre_id=semestre.id ) ) + # On récupère les assiduités et les justificatifs de l'étudiant assiduites = scass.filter_by_formsemestre( etud.assiduites, Assiduite, semestre ).order_by(Assiduite.entry_date.desc()) @@ -1225,10 +1254,17 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: etud.justificatifs, Justificatif, semestre ).order_by(Justificatif.entry_date.desc()) + # On calcule les statistiques stats: dict = scass.get_assiduites_stats( assiduites, metric=metrique, filtered={"split": True} ) + # On sépare : + # - abs_j = absences justifiées + # - abs_nj = absences non justifiées + # - retards = les retards + # - justifs = les justificatifs + abs_j: list[str] = [ {"date": _get_date_str(assi.date_debut, assi.date_fin)} for assi in assiduites @@ -1275,13 +1311,36 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str: + """ + _get_date_str transforme une période en chaîne lisible + + Args: + deb (datetime.datetime): date de début + fin (datetime.datetime): date de fin + + Returns: + str: + "le dd/mm/yyyy de hh:MM à hh:MM" si les deux date sont sur le même jour + "du dd/mm/yyyy hh:MM audd/mm/yyyy hh:MM" sinon + """ if deb.date() == fin.date(): temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime("%H:%M")] return f"le {temps[0]} de {temps[1]} à {temps[2]}" return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}' -def _get_days_between_dates(deb: str, fin: str): +def _get_days_between_dates(deb: str, fin: str) -> str: + """ + _get_days_between_dates récupère tous les jours entre deux dates + + Args: + deb (str): date de début + fin (str): date de fin + + Returns: + str: une chaine json représentant une liste des jours + ['date_iso','date_iso2', ...] + """ if deb is None or fin is None: return "null" try: @@ -1301,8 +1360,25 @@ def _get_days_between_dates(deb: str, fin: str): def _differee( - etudiants, moduleimpl_select, date=None, periode=None, formsemestre_id=None -): + etudiants: list[dict], + moduleimpl_select: str, + date: str = None, + periode: dict[str, str] = None, + formsemestre_id: int = None, +) -> str: + """ + _differee Génère un tableau de saisie différé + + Args: + etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires) + moduleimpl_select (str): l'html représentant le selecteur de module + date (str, optional): la première date à afficher. Defaults to None. + periode (dict[str, str], optional):La période par défaut de la première colonne. Defaults to None. + formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. Defaults to None. + + Returns: + str: le widget (html/css/js) + """ if date is None: date = datetime.date.today().isoformat() @@ -1329,9 +1405,7 @@ def _differee( ) -def _module_selector( - formsemestre: FormSemestre, moduleimpl_id: int = None -) -> HTMLElement: +def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str: """ _module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre @@ -1341,18 +1415,26 @@ def _module_selector( Returns: str: La représentation str d'un HTMLSelectElement """ - + # 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] = [] - ues = ntc.get_ues_stat_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"]) + # 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" - modules = [] + # Vérification que le moduleimpl_id passé en paramètre est bien un entier + try: + moduleimpl_id = int(moduleimpl_id) + except (ValueError, TypeError): + moduleimpl_id = None + modules: list[dict[str, str | int]] = [] + # Récupération de l'id et d'un nom lisible pour chaque moduleimpl for modimpl in modimpls_list: modname: str = ( (modimpl["module"]["code"] or "") @@ -1361,11 +1443,6 @@ def _module_selector( ) modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname}) - try: - moduleimpl_id = int(moduleimpl_id) - except (ValueError, TypeError): - moduleimpl_id = None - return render_template( "assiduites/widgets/moduleimpl_selector.j2", selected=selected, @@ -1374,13 +1451,30 @@ def _module_selector( ) -def _dynamic_module_selector(): +def _dynamic_module_selector() -> str: + """ + _dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique + + Returns: + str: l'html/css/javascript du selecteur de module dynamique + """ return render_template( "assiduites/widgets/moduleimpl_dynamic_selector.j2", ) -def _timeline(formsemestre_id=None) -> HTMLElement: +def _timeline(formsemestre_id: int = None) -> str: + """ + _timeline retourne l'html de la timeline + + Args: + formsemestre_id (int, optional): un formsemestre. Defaults to None. + Le formsemestre sert à obtenir la période par défaut de la timeline + sinon ce sera de 2 heure dès le début de la timeline + + Returns: + str: l'html en chaîne de caractères + """ return render_template( "assiduites/widgets/timeline.j2", t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"), @@ -1392,7 +1486,13 @@ def _timeline(formsemestre_id=None) -> HTMLElement: ) -def _mini_timeline() -> HTMLElement: +def _mini_timeline() -> str: + """ + _mini_timeline Retourne l'html lié au mini timeline d'assiduités + + Returns: + str: l'html en chaîne de caractères + """ return render_template( "assiduites/widgets/minitimeline.j2", t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"), diff --git a/tools/downgrade_assiduites.py b/tools/downgrade_assiduites.py index dd87981a0d..2a6ddd0805 100644 --- a/tools/downgrade_assiduites.py +++ b/tools/downgrade_assiduites.py @@ -26,6 +26,7 @@ def downgrade_module( dept_etudid: list[int] = None dept_id: int = None + # Récupération du département si spécifié if dept is not None: departement: Departement = Departement.query.filter_by(acronym=dept).first() @@ -34,13 +35,16 @@ def downgrade_module( dept_etudid = [etud.id for etud in departement.etudiants] dept_id = departement.id + # Suppression des assiduités if assiduites: _remove_assiduites(dept_etudid) - + # Suppression des justificatifs if justificatifs: _remove_justificatifs(dept_etudid) _remove_justificatifs_archive(dept_id) + # Si on supprime tout le module assiduité/justificatif alors on remet à zero + # les séquences postgres if dept is None: if assiduites: db.session.execute( @@ -51,26 +55,52 @@ def downgrade_module( sa.text("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1") ) + # On valide l'opération sur la bdd db.session.commit() + # On affiche un message pour l'utilisateur print( f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}" ) def _remove_assiduites(dept_etudid: str = None): + """ + _remove_assiduites Supprime les assiduités + + Args: + dept_etudid (str, optional): la liste des etudid d'un département. Defaults to None. + """ if dept_etudid is None: + # Si pas d'étudids alors on supprime toutes les assiduités Assiduite.query.delete() else: + # Sinon on supprime que les assiduités des étudiants donnés Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete() def _remove_justificatifs(dept_etudid: str = None): + """ + _remove_justificatifs Supprime les justificatifs + + Args: + dept_etudid (str, optional): la liste des etudid d'un département. Defaults to None. + """ if dept_etudid is None: + # Si pas d'étudids alors on supprime tous les justificatifs Justificatif.query.delete() else: + # Sinon on supprime que les justificatifs des étudiants donnés Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete() def _remove_justificatifs_archive(dept_id: int = None): + """ + _remove_justificatifs_archive On supprime les archives des fichiers justificatifs + + Args: + dept_id (int, optional): l'id du département à supprimer . Defaults to None. + Si none : supprime tous les département + Sinon uniquement le département sélectionné + """ JustificatifArchiver().remove_dept_archive(dept_id)