############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 API : Assiduités""" from datetime import datetime, timedelta import re from flask import g, request from flask_json import as_json from flask_login import current_user, login_required from flask_sqlalchemy.query import Query from sqlalchemy.orm.exc import ObjectDeletedError from werkzeug.exceptions import HTTPException from app import db, log, set_sco_dept import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app.api import api_bp as bp from app.api import api_web_bp, get_model_api_object, tools from app.api import api_permission_required as permission_required from app.decorators import scodoc from app.models import ( Assiduite, Evaluation, FormSemestre, Identite, ModuleImpl, Scolog, ) from app.models.assiduites import ( get_assiduites_justif, get_justifs_from_date, get_formsemestre_from_data, ) from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @bp.route("/assiduite/date_time_offset/") @api_web_bp.route("/assiduite/date_time_offset/") @scodoc @permission_required(Permission.ScoView) def date_time_offset(date_iso: str): """L'offset dans le fuseau horaire du serveur pour la date indiquée. Renvoie une chaîne de la forme "+04:00" (ISO 8601) Exemple: `/assiduite/date_time_offset/2024-10-01` renvoie `'+02:00'` """ if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_iso): json_error( 404, message="date invalide", ) return scu.get_local_timezone_offset(date_iso) @bp.route("/assiduite/") @api_web_bp.route("/assiduite/") @scodoc @permission_required(Permission.ScoView) @as_json def assiduite(assiduite_id: int = None): """Retourne un objet assiduité à partir de son id Exemple de résultat: ```json { "assiduite_id": 1, "etudid": 2, "moduleimpl_id": 3, "date_debut": "2022-10-31T08:00+01:00", "date_fin": "2022-10-31T10:00+01:00", "etat": "retard", "desc": "une description", "user_id": 1 or null, "user_name" : login scodoc or null, "user_nom_complet": "Marie Dupont", "est_just": False or True, } ``` SAMPLES ------- /assiduite/1; """ return get_model_api_object(Assiduite, assiduite_id, Identite) @bp.route("/assiduite//justificatifs", defaults={"long": False}) @api_web_bp.route( "/assiduite//justificatifs", defaults={"long": False} ) @bp.route("/assiduite//justificatifs/long", defaults={"long": True}) @api_web_bp.route( "/assiduite//justificatifs/long", defaults={"long": True} ) @scodoc @permission_required(Permission.ScoView) @as_json def assiduite_justificatifs(assiduite_id: int = None, long: bool = False): """Retourne la liste des justificatifs qui justifient cette assiduité. Exemple de résultat: ```json [ 1, 2, 3, ... ] ``` SAMPLES ------- /assiduite/1/justificatifs; /assiduite/1/justificatifs/long; """ return get_assiduites_justif(assiduite_id, long) # etudid @bp.route("/assiduites//count", defaults={"with_query": False}) @api_web_bp.route("/assiduites//count", defaults={"with_query": False}) @bp.route("/assiduites//count/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites//count/query", defaults={"with_query": True}) @bp.route("/assiduites/etudid//count", defaults={"with_query": False}) @api_web_bp.route( "/assiduites/etudid//count", defaults={"with_query": False} ) @bp.route("/assiduites/etudid//count/query", defaults={"with_query": True}) @api_web_bp.route( "/assiduites/etudid//count/query", defaults={"with_query": True} ) # nip @bp.route("/assiduites/nip//count", defaults={"with_query": False}) @api_web_bp.route("/assiduites/nip//count", defaults={"with_query": False}) @bp.route("/assiduites/nip//count/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/nip//count/query", defaults={"with_query": True}) # ine @bp.route("/assiduites/ine//count", defaults={"with_query": False}) @api_web_bp.route("/assiduites/ine//count", defaults={"with_query": False}) @bp.route("/assiduites/ine//count/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/ine//count/query", defaults={"with_query": True}) # @login_required @scodoc @as_json @permission_required(Permission.ScoView) def assiduites_count( etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False ): """ Retourne le nombre d'assiduités d'un étudiant. QUERY ----- user_id: est_just: moduleimpl_id: date_debut: date_fin: etat: formsemestre_id: metric: split: PARAMS ----- user_id:l'id de l'auteur de l'assiduité est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) moduleimpl_id:l'id du module concerné par l'assiduité date_debut:date de début de l'assiduité (supérieur ou égal) date_fin:date de fin de l'assiduité (inférieur ou égal) etat:etat de l'étudiant → absent, present ou retard formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité metric: la/les métriques de comptage (journee, demi, heure, compte) split: divise le comptage par état SAMPLES ------- /assiduites/1/count; /assiduites/1/count/query?etat=retard; /assiduites/1/count/query?split; /assiduites/1/count/query?etat=present,retard&metric=compte,heure; """ # 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", ) set_sco_dept(etud.departement.acronym) # 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) return scass.get_assiduites_stats( assiduites=etud.assiduites, metric=metric, filtered=filtered ) # etudid @bp.route("/assiduites/", defaults={"with_query": False}) @api_web_bp.route("/assiduites/", defaults={"with_query": False}) @bp.route("/assiduites//query", defaults={"with_query": True}) @api_web_bp.route("/assiduites//query", defaults={"with_query": True}) @bp.route("/assiduites/etudid/", defaults={"with_query": False}) @api_web_bp.route("/assiduites/etudid/", defaults={"with_query": False}) @bp.route("/assiduites/etudid//query", defaults={"with_query": True}) @api_web_bp.route( "/assiduites/etudid//query", defaults={"with_query": True} ) # nip @bp.route("/assiduites/nip/", defaults={"with_query": False}) @api_web_bp.route("/assiduites/nip/", defaults={"with_query": False}) @bp.route("/assiduites/nip//query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/nip//query", defaults={"with_query": True}) # ine @bp.route("/assiduites/ine/", defaults={"with_query": False}) @api_web_bp.route("/assiduites/ine/", defaults={"with_query": False}) @bp.route("/assiduites/ine//query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/ine//query", defaults={"with_query": True}) # @login_required @scodoc @as_json @permission_required(Permission.ScoView) def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False): """ Retourne toutes les assiduités d'un étudiant QUERY ----- user_id: est_just: moduleimpl_id: date_debut: date_fin: etat: formsemestre_id: with_justifs: PARAMS ----- user_id:l'id de l'auteur de l'assiduité est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) moduleimpl_id:l'id du module concerné par l'assiduité date_debut:date de début de l'assiduité (supérieur ou égal) date_fin:date de fin de l'assiduité (inférieur ou égal) etat:etat de l'étudiant → absent, present ou retard formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité with_justif:ajoute les justificatifs liés à l'assiduité SAMPLES ------- /assiduites/1; /assiduites/1/query?etat=retard; /assiduites/1/query?moduleimpl_id=1; /assiduites/1/query?with_justifs=; """ # 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", ) # 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 @bp.route("/assiduites//evaluations") @api_web_bp.route("/assiduites//evaluations") # etudid @bp.route("/assiduites/etudid//evaluations") @api_web_bp.route("/assiduites/etudid//evaluations") # ine @bp.route("/assiduites/ine//evaluations") @api_web_bp.route("/assiduites/ine//evaluations") # nip @bp.route("/assiduites/nip//evaluations") @api_web_bp.route("/assiduites/nip//evaluations") @login_required @scodoc @as_json @permission_required(Permission.ScoView) def assiduites_evaluations(etudid: int = None, nip=None, ine=None): """ Retourne la liste de toutes les évaluations de l'étudiant Pour chaque évaluation, retourne la liste des objets assiduités sur la plage de l'évaluation Exemple de résultat: ```json [ { "evaluation_id": 1234, "assiduites": [ { "assiduite_id":1234, ... }, ] } ] SAMPLES ------- /assiduites/1/evaluations; ``` """ # 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", ) # Récupération des évaluations et des assidiutés etud_evaluations_assiduites: list[dict] = scass.get_etud_evaluations_assiduites( etud ) return etud_evaluations_assiduites @api_web_bp.route("/evaluation//assiduites") @bp.route("/evaluation//assiduites") @login_required @scodoc @as_json @permission_required(Permission.ScoView) def evaluation_assiduites(evaluation_id): """ Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation Exemple de résultat: ```json { "" : [ { "assiduite_id":1234, ... }, ] } ``` CATEGORY -------- Évaluations """ # Récupération de l'évaluation try: evaluation: Evaluation = Evaluation.get_evaluation(evaluation_id) except HTTPException: return json_error(404, "L'évaluation n'existe pas") evaluation_assiduites_par_etudid: dict[int, list[Assiduite]] = {} for assi in scass.get_evaluation_assiduites(evaluation): etudid: str = str(assi.etudid) etud_assiduites = evaluation_assiduites_par_etudid.get(etudid, []) etud_assiduites.append(assi.to_dict(format_api=True)) evaluation_assiduites_par_etudid[etudid] = etud_assiduites return evaluation_assiduites_par_etudid @bp.route("/assiduites/group/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/group/query", defaults={"with_query": True}) @login_required @scodoc @as_json @permission_required(Permission.ScoView) def assiduites_group(with_query: bool = False): """ Retourne toutes les assiduités d'un groupe d'étudiants chemin : /assiduites/group/query?etudids=1,2,3 QUERY ----- user_id: est_just: moduleimpl_id: date_debut: date_fin: etat: etudids: formsemestre_id: with_justif: PARAMS ----- user_id:l'id de l'auteur de l'assiduité est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) moduleimpl_id:l'id du module concerné par l'assiduité date_debut:date de début de l'assiduité (supérieur ou égal) date_fin:date de fin de l'assiduité (inférieur ou égal) etat:etat de l'étudiant → absent, present ou retard etudids:liste des ids des étudiants concernés par la recherche formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité with_justifs:ajoute les justificatifs liés à l'assiduité SAMPLES ------- /assiduites/group/query?etudids=1,2,3; """ # Récupération des étudiants dans la requête etuds = request.args.get("etudids", "") etuds = etuds.split(",") try: etuds = [int(etu) for etu in etuds] except ValueError: return json_error(404, "Le champ 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) if len(etuds) != query.count() or len(etuds) == 0: return json_error( 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 @bp.route( "/assiduites/formsemestre/", defaults={"with_query": False} ) @api_web_bp.route( "/assiduites/formsemestre/", defaults={"with_query": False} ) @bp.route( "/assiduites/formsemestre//query", defaults={"with_query": True}, ) @api_web_bp.route( "/assiduites/formsemestre//query", defaults={"with_query": True}, ) @login_required @scodoc @as_json @permission_required(Permission.ScoView) def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): """Retourne toutes les assiduités du formsemestre QUERY ----- user_id: est_just: moduleimpl_id: date_debut: date_fin: etat: PARAMS ----- user_id:l'id de l'auteur de l'assiduité est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) moduleimpl_id:l'id du module concerné par l'assiduité date_debut:date de début de l'assiduité (supérieur ou égal) date_fin:date de fin de l'assiduité (inférieur ou égal) etat:etat de l'étudiant → absent, present ou retard formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité SAMPLES ------- /assiduites/formsemestre/1; /assiduites/formsemestre/1/query?etat=retard; /assiduites/formsemestre/1/query?moduleimpl_id=1; """ # 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) data = _with_justifs(data) data_set.append(data) return data_set @bp.route( "/assiduites/formsemestre//count", defaults={"with_query": False}, ) @api_web_bp.route( "/assiduites/formsemestre//count", defaults={"with_query": False}, ) @bp.route( "/assiduites/formsemestre//count/query", defaults={"with_query": True}, ) @api_web_bp.route( "/assiduites/formsemestre//count/query", defaults={"with_query": True}, ) @login_required @scodoc @as_json @permission_required(Permission.ScoView) def assiduites_formsemestre_count( formsemestre_id: int = None, with_query: bool = False ): """Comptage des assiduités du formsemestre QUERY ----- user_id: est_just: moduleimpl_id: date_debut: date_fin: etat: formsemestre_id: metric: split: PARAMS ----- user_id:l'id de l'auteur de l'assiduité est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) moduleimpl_id:l'id du module concerné par l'assiduité date_debut:date de début de l'assiduité (supérieur ou égal) date_fin:date de fin de l'assiduité (inférieur ou égal) etat:etat de l'étudiant → absent, present ou retard formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité metric: la/les métriques de comptage (journee, demi, heure, compte) split: divise le comptage par état SAMPLES ------- /assiduites/formsemestre/1/count; /assiduites/formsemestre/1/count/query?etat=retard; /assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure; """ # 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") set_sco_dept(formsemestre.departement.acronym) # 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) return scass.get_assiduites_stats(assiduites_query, metric, filtered) # etudid @bp.route("/assiduite//create", methods=["POST"]) @api_web_bp.route("/assiduite//create", methods=["POST"]) @bp.route("/assiduite/etudid//create", methods=["POST"]) @api_web_bp.route("/assiduite/etudid//create", methods=["POST"]) # nip @bp.route("/assiduite/nip//create", methods=["POST"]) @api_web_bp.route("/assiduite/nip//create", methods=["POST"]) # ine @bp.route("/assiduite/ine//create", methods=["POST"]) @api_web_bp.route("/assiduite/ine//create", methods=["POST"]) # @scodoc @as_json @login_required @permission_required(Permission.AbsChange) def assiduite_create(etudid: int = None, nip=None, ine=None): """ Enregistrement d'assiduités pour un étudiant (etudid). Si les heures n'ont pas de timezone, elles sont exprimées dans celle du serveur. DATA ---- ```json [ { "date_debut": str, "date_fin": str, "etat": str, }, { "date_debut": str, "date_fin": str, "etat": str, "moduleimpl_id": int, "desc":str, } ... ] ``` SAMPLES ------- /assiduite/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}] /assiduite/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}] """ # 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") # 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}) else: success.append({"indice": i, "message": obj}) scass.simple_invalidate_cache(data, etud.id) db.session.commit() return {"errors": errors, "success": success} @bp.route("/assiduites/create", methods=["POST"]) @api_web_bp.route("/assiduites/create", methods=["POST"]) @scodoc @as_json @login_required @permission_required(Permission.AbsChange) def assiduites_create(): """ Création d'une assiduité ou plusieurs assiduites DATA ---- ```json [ { "date_debut": str, "date_fin": str, "etat": str, "etudid":int, }, { "date_debut": str, "date_fin": str, "etat": str, "etudid":int, "moduleimpl_id": int, "desc":str, } ... ] ``` SAMPLES ------- /assiduites/create;[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}] /assiduites/create;[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}] """ 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 = [] for i, data in enumerate(create_list): etud: Identite = Identite.query.filter_by(id=data["etudid"]).first() if etud is None: errors.append({"indice": i, "message": "Cet étudiant n'existe pas."}) continue if g.scodoc_dept is None and etud.dept_id is not None: # route sans département set_sco_dept(etud.departement.acronym) code, obj = _create_one(data, etud) if code == 404: errors.append({"indice": i, "message": obj}) else: success.append({"indice": i, "message": obj}) scass.simple_invalidate_cache(data) return {"errors": errors, "success": success} def _create_one( data: dict, etud: Identite, ) -> tuple[int, object]: """ Création d'une assiduité à partir d'un dict Cette fonction vérifie les données du dict (qui vient du JSON API) 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: 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 = scu.EtatAssiduite.get(etat) # cas 2 : date_debut date_debut: str = data.get("date_debut", None) if date_debut is None: errors.append("param 'date_debut': manquant") # 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: # Mise à jour de la timezone avec celle du serveur deb: datetime = scu.localize_datetime(deb) # 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: 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: datetime = scu.localize_datetime(fin) # check duration: min 1 minute if (deb is not None) and (fin is not None) and (fin - deb) < timedelta(seconds=60): errors.append("durée trop courte") # cas 4 : desc desc: str = data.get("desc", 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 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() except ValueError: moduleimpl = None 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: 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) # SI TOUT EST OK try: # On essaye de créer l'assiduité nouv_assiduite: Assiduite = Assiduite.create_assiduite( date_debut=deb, date_fin=fin, etat=etat, etud=etud, moduleimpl=moduleimpl, description=desc, user_id=current_user.id, external_data=external_data, 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] @bp.route("/assiduite/delete", methods=["POST"]) @api_web_bp.route("/assiduite/delete", methods=["POST"]) @login_required @scodoc @as_json @permission_required(Permission.AbsChange) def assiduite_delete(): """ Suppression d'une assiduité à partir de son id DATA ---- ```json [ , ... ] ``` SAMPLES ------- /assiduite/delete;[2,2,3] """ # 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": []} # 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: output["success"].append({"indice": i, "message": "OK"}) db.session.commit() return output 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: # 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) # 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}", ) db.session.delete(assiduite_unique) # Invalidation du cache scass.simple_invalidate_cache(assi_dict) return 200, "OK" @bp.route("/assiduite//edit", methods=["POST"]) @api_web_bp.route("/assiduite//edit", methods=["POST"]) @login_required @scodoc @as_json @permission_required(Permission.AbsChange) def assiduite_edit(assiduite_id: int): """ Edition d'une assiduité à partir de son id DATA ---- ```json { "etat"?: str, "moduleimpl_id"?: int "desc"?: str "est_just"?: bool } ``` SAMPLES ------- /assiduite/1/edit;{""etat"":""absent""} /assiduite/1/edit;{""moduleimpl_id"":2} /assiduite/1/edit;{""etat"": ""retard"",""moduleimpl_id"":3} """ # Récupération de l'assiduité à modifier assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: return json_error(404, "Assiduité non existante") # Récupération des valeurs à modifier data = request.get_json(force=True) # 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", assiduite_unique.etudiant.id, msg=f"assiduite: modif {assiduite_unique}", ) db.session.commit() try: scass.simple_invalidate_cache(assiduite_unique.to_dict()) except ObjectDeletedError: return json_error(404, "Assiduité supprimée / inexistante") return {"OK": True} @bp.route("/assiduites/edit", methods=["POST"]) @api_web_bp.route("/assiduites/edit", methods=["POST"]) @login_required @scodoc @as_json @permission_required(Permission.AbsChange) def assiduites_edit(): """ Edition de plusieurs assiduités DATA ---- ```json [ { "assiduite_id" : int, "etat"?: str, "moduleimpl_id"?: int "desc"?: str "est_just"?: bool } ] ``` SAMPLES ------- /assiduites/edit;[{""etat"":""absent"",""assiduite_id"":1}] /assiduites/edit;[{""moduleimpl_id"":2,""assiduite_id"":1}] /assiduites/edit;[{""etat"": ""retard"",""moduleimpl_id"":3,""assiduite_id"":1}] """ edit_list: list[object] = request.get_json(force=True) if not isinstance(edit_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") errors: list[dict] = [] success: list[dict] = [] for i, data in enumerate(edit_list): assi: Identite = Assiduite.query.filter_by(id=data["assiduite_id"]).first() if assi is None: errors.append( { "indice": i, "message": f"assiduité {data['assiduite_id']} n'existe pas.", } ) continue code, obj = _edit_one(assi, data) obj_retour = { "indice": i, "message": obj, } if code == 404: errors.append(obj_retour) else: success.append(obj_retour) db.session.commit() return {"errors": errors, "success": success} 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 = 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 # 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 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: dict = ( external_data if external_data is not None and isinstance(external_data, dict) else assiduite_unique.external_data ) 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) ).first() except ValueError: moduleimpl = None if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: if not moduleimpl.est_inscrit(assiduite_unique.etudiant): 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() ) force: bool if formsemestre: force = scu.is_assiduites_module_forced(formsemestre_id=formsemestre.id) else: force = scu.is_assiduites_module_forced( dept_id=assiduite_unique.etudiant.dept_id ) external_data = ( external_data if external_data is not None and isinstance(external_data, dict) else assiduite_unique.external_data ) if force and not ( external_data is not None and external_data.get("module", False) != "" ): errors.append( "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" ) # Cas 4 : desc desc: str = data.get("desc", False) if desc is not False: assiduite_unique.description = desc # Cas 5 : est_just if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: assiduite_unique.est_just = False else: assiduite_unique.est_just = ( len( get_justifs_from_date( assiduite_unique.etudiant.id, assiduite_unique.date_debut, assiduite_unique.date_fin, valid=True, ) ) > 0 ) 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", assiduite_unique.etudiant.id, msg=f"assiduite: modif {assiduite_unique}", ) db.session.add(assiduite_unique) scass.simple_invalidate_cache(assiduite_unique.to_dict()) return (200, "OK") # -- Utils -- def _count_manager(requested) -> tuple[str, dict]: """ Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête """ filtered: dict = {} # cas 1 : etat assiduite etat = requested.args.get("etat") if etat is not None: filtered["etat"] = etat # cas 2 : date de début deb = requested.args.get("date_debut", "").replace(" ", "+") deb: datetime = scu.is_iso_formated(deb, True) if deb is not None: filtered["date_debut"] = deb # cas 3 : date de fin fin = requested.args.get("date_fin", "").replace(" ", "+") fin = scu.is_iso_formated(fin, True) if fin is not None: filtered["date_fin"] = fin # cas 4 : moduleimpl_id module = requested.args.get("moduleimpl_id", False) try: if module is False: raise ValueError if module != "": module = int(module) else: module = None except ValueError: module = False if module is not False: filtered["moduleimpl_id"] = module # cas 5 : formsemestre_id formsemestre_id = requested.args.get("formsemestre_id") if formsemestre_id is not None: formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() filtered["formsemestre"] = formsemestre # cas 6 : type metric = requested.args.get("metric", "all") # cas 7 : est_just est_just: str = requested.args.get("est_just") if est_just is not None: trues: tuple[str] = ("v", "t", "vrai", "true", "1") falses: tuple[str] = ("f", "faux", "false", "0") if est_just.lower() in trues: filtered["est_just"] = True elif est_just.lower() in falses: filtered["est_just"] = False # cas 8 : user_id user_id = requested.args.get("user_id", False) if user_id is not False: filtered["user_id"] = user_id # cas 9 : split split = requested.args.get("split", False) if split is not False: filtered["split"] = True return (metric, filtered) def _filter_manager(requested, assiduites_query: Query) -> Query: """ _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: 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: 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: 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 ) # cas 4 : moduleimpl_id module = requested.args.get("moduleimpl_id", False) try: if module is False: raise ValueError if module != "": module = int(module) else: module = None except ValueError: module = False if module is not False: assiduites_query = scass.filter_by_module_impl(assiduites_query, module) # cas 5 : formsemestre_id formsemestre_id = requested.args.get("formsemestre_id") if formsemestre_id is not None: formsemestre: FormSemestre = None formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() assiduites_query = scass.filter_by_formsemestre( assiduites_query, Assiduite, formsemestre ) # cas 6 : est_just est_just: str = requested.args.get("est_just") if est_just is not None: trues: tuple[str] = ("v", "t", "vrai", "true", "1") falses: tuple[str] = ("f", "faux", "false", "0") if est_just.lower() in trues: assiduites_query: Query = scass.filter_assiduites_by_est_just( assiduites_query, True ) elif est_just.lower() in falses: assiduites_query: Query = scass.filter_assiduites_by_est_just( assiduites_query, False ) # cas 8 : user_id user_id = requested.args.get("user_id", False) 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() assiduites_query: Query = assiduites_query.filter( Assiduite.date_debut >= scu.date_debut_annee_scolaire(annee), Assiduite.date_fin <= scu.date_fin_annee_scolaire(annee), ) return assiduites_query 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) return assi