############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 API : Assiduités""" from datetime import datetime 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 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.decorators import permission_required, scodoc from app.models import ( Assiduite, 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/") @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: { "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, } """ 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 justifie cette assiduitée Exemple de résultat: [ 1, 2, 3, ... ] """ 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 count_assiduites( etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False ): """ Retourne le nombre d'assiduités d'un étudiant chemin : /assiduites//count Un filtrage peut être donné avec une query chemin : /assiduites//count/query? Les différents filtres : Type (type de comptage -> journee, demi, heure, nombre d'assiduite): query?type=(journee, demi, heure) -> une seule valeur parmis les trois ex: .../query?type=heure Comportement par défaut : compte le nombre d'assiduité enregistrée Etat (etat de l'étudiant -> absent, present ou retard): query?etat=[- liste des états séparé par une virgule -] ex: .../query?etat=present,retard Date debut (date de début de l'assiduité, sont affichés les assiduités dont la date de début est supérieur ou égale à la valeur donnée): query?date_debut=[- date au format iso -] ex: query?date_debut=2022-11-03T08:00+01:00 Date fin (date de fin de l'assiduité, sont affichés les assiduités dont la date de fin est inférieure ou égale à la valeur donnée): query?date_fin=[- date au format iso -] ex: query?date_fin=2022-11-03T10:00+01:00 Moduleimpl_id (l'id du module concerné par l'assiduité): query?moduleimpl_id=[- int ou vide -] ex: query?moduleimpl_id=1234 query?moduleimpl_od= Formsemstre_id (l'id du formsemestre concerné par l'assiduité) query?formsemestre_id=[int] ex query?formsemestre_id=3 user_id (l'id de l'auteur de l'assiduité) query?user_id=[int] ex query?user_id=3 est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard)) query?est_just=[bool] query?est_just=f query?est_just=t """ # 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 chemin : /assiduites/ Un filtrage peut être donné avec une query chemin : /assiduites//query? Les différents filtres : Etat (etat de l'étudiant -> absent, present ou retard): query?etat=[- liste des états séparé par une virgule -] ex: .../query?etat=present,retard Date debut (date de début de l'assiduité, sont affichés les assiduités dont la date de début est supérieur ou égale à la valeur donnée): query?date_debut=[- date au format iso -] ex: query?date_debut=2022-11-03T08:00+01:00 Date fin (date de fin de l'assiduité, sont affichés les assiduités dont la date de fin est inférieure ou égale à la valeur donnée): query?date_fin=[- date au format iso -] ex: query?date_fin=2022-11-03T10:00+01:00 Moduleimpl_id (l'id du module concerné par l'assiduité): query?moduleimpl_id=[- int ou vide -] ex: query?moduleimpl_id=1234 query?moduleimpl_od= Formsemstre_id (l'id du formsemestre concerné par l'assiduité) query?formsemstre_id=[int] ex query?formsemestre_id=3 user_id (l'id de l'auteur de l'assiduité) query?user_id=[int] ex query?user_id=3 est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard)) query?est_just=[bool] query?est_just=f query?est_just=t """ # 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/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 Un filtrage peut être donné avec une query chemin : /assiduites/group/query?etudids=1,2,3 Les différents filtres : Etat (etat de l'étudiant -> absent, present ou retard): query?etat=[- liste des états séparé par une virgule -] ex: .../query?etat=present,retard Date debut (date de début de l'assiduité, sont affichés les assiduités dont la date de début est supérieur ou égale à la valeur donnée): query?date_debut=[- date au format iso -] ex: query?date_debut=2022-11-03T08:00+01:00 Date fin (date de fin de l'assiduité, sont affichés les assiduités dont la date de fin est inférieure ou égale à la valeur donnée): query?date_fin=[- date au format iso -] ex: query?date_fin=2022-11-03T10:00+01:00 Moduleimpl_id (l'id du module concerné par l'assiduité): query?moduleimpl_id=[- int ou vide -] ex: query?moduleimpl_id=1234 query?moduleimpl_od= Formsemstre_id (l'id du formsemestre concerné par l'assiduité) query?formsemstre_id=[int] ex query?formsemestre_id=3 user_id (l'id de l'auteur de l'assiduité) query?user_id=[int] ex query?user_id=3 est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard)) query?est_just=[bool] query?est_just=f query?est_just=t """ # 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""" # 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 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") 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) La requête doit avoir un content type "application/json": [ { "date_debut": str, "date_fin": str, "etat": str, }, { "date_debut": str, "date_fin": str, "etat": str, "moduleimpl_id": int, "desc":str, } ... ] """ # 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 La requête doit avoir un content type "application/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, } ... ] """ 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) # 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 Forme des données envoyées : [ , ... ] """ # 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 La requête doit avoir un content type "application/json": { "etat"?: str, "moduleimpl_id"?: int "desc"?: str "est_just"?: bool } """ # 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 La requête doit avoir un content type "application/json": [ { "assiduite_id" : int, "etat"?: str, "moduleimpl_id"?: int "desc"?: str "est_just"?: bool } ] """ 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