############################################################################## # ScoDoc # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc 9 API : Justificatifs """ from datetime import datetime from flask_json import as_json from flask import g, request from flask_login import login_required, current_user from flask_sqlalchemy.query import Query from werkzeug.exceptions import NotFound import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app import db, set_sco_dept from app.api import api_bp as bp from app.api import api_web_bp from app.api import get_model_api_object, tools from app.decorators import permission_required, scodoc from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog from app.models.assiduites import ( get_formsemestre_from_data, ) from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error from app.scodoc.sco_groups import get_group_members # Partie Modèle @bp.route("/justificatif/") @api_web_bp.route("/justificatif/") @scodoc @permission_required(Permission.ScoView) def justificatif(justif_id: int = None): """Retourne un objet justificatif à partir de son id Exemple de résultat: { "justif_id": 1, "etudid": 2, "date_debut": "2022-10-31T08:00+01:00", "date_fin": "2022-10-31T10:00+01:00", "etat": "valide", "fichier": "archive_id", "raison": "une raison", // VIDE si pas le droit "entry_date": "2022-10-31T08:00+01:00", "user_id": 1 or null, } """ return get_model_api_object( Justificatif, justif_id, Identite, restrict=not current_user.has_permission(Permission.AbsJustifView), ) # etudid @bp.route("/justificatifs/", defaults={"with_query": False}) @api_web_bp.route("/justificatifs/", defaults={"with_query": False}) @bp.route("/justificatifs//query", defaults={"with_query": True}) @api_web_bp.route("/justificatifs//query", defaults={"with_query": True}) @bp.route("/justificatifs/etudid/", defaults={"with_query": False}) @api_web_bp.route("/justificatifs/etudid/", defaults={"with_query": False}) @bp.route("/justificatifs/etudid//query", defaults={"with_query": True}) @api_web_bp.route( "/justificatifs/etudid//query", defaults={"with_query": True} ) # nip @bp.route("/justificatifs/nip/", defaults={"with_query": False}) @api_web_bp.route("/justificatifs/nip/", defaults={"with_query": False}) @bp.route("/justificatifs/nip//query", defaults={"with_query": True}) @api_web_bp.route("/justificatifs/nip//query", defaults={"with_query": True}) # ine @bp.route("/justificatifs/ine/", defaults={"with_query": False}) @api_web_bp.route("/justificatifs/ine/", defaults={"with_query": False}) @bp.route("/justificatifs/ine//query", defaults={"with_query": True}) @api_web_bp.route("/justificatifs/ine//query", defaults={"with_query": True}) # @login_required @scodoc @as_json @permission_required(Permission.ScoView) def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False): """ Retourne toutes les assiduités d'un étudiant chemin : /justificatifs/ Un filtrage peut être donné avec une query chemin : /justificatifs//query? Les différents filtres : Etat (etat du justificatif -> validé, non validé, modifé, en attente): query?etat=[- liste des états séparé par une virgule -] ex: .../query?etat=validé,modifié Date debut (date de début du justificatif, sont affichés les justificatifs 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 du justificatif, sont affichés les justificatifs 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 user_id (l'id de l'auteur du justificatif) 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: return json_error( 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] = [] restrict = not current_user.has_permission(Permission.AbsJustifView) for just in justificatifs_query.all(): data = just.to_dict(format_api=True, restrict=restrict) data_set.append(data) return data_set @api_web_bp.route("/justificatifs/dept/", defaults={"with_query": False}) @api_web_bp.route( "/justificatifs/dept//query", defaults={"with_query": True} ) @bp.route("/justificatifs/dept/", defaults={"with_query": False}) @bp.route("/justificatifs/dept//query", defaults={"with_query": True}) @login_required @scodoc @as_json @permission_required(Permission.ScoView) def justificatifs_dept(dept_id: int = None, with_query: bool = False): """ Renvoie tous les justificatifs d'un département (en ajoutant un champ "formsemestre" si possible) """ # Récupération du département et des étudiants du département dept: Departement = Departement.query.get(dept_id) if dept is None: return json_error(404, "Assiduité non existante") 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: Query = _filter_manager(request, justificatifs_query) # Mise en forme des données et retour JSON restrict = not current_user.has_permission(Permission.AbsJustifView) data_set: list[dict] = [] for just in justificatifs_query: data_set.append(_set_sems(just, restrict=restrict)) return data_set def _set_sems(justi: Justificatif, restrict: bool) -> 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, restrict=restrict) # 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 @bp.route( "/justificatifs/formsemestre/", defaults={"with_query": False} ) @api_web_bp.route( "/justificatifs/formsemestre/", defaults={"with_query": False} ) @bp.route( "/justificatifs/formsemestre//query", defaults={"with_query": True}, ) @api_web_bp.route( "/justificatifs/formsemestre//query", defaults={"with_query": True}, ) @login_required @scodoc @as_json @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 = 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 justificatifs du semestre justificatifs_query: Query = scass.filter_by_formsemestre( Justificatif.query, Justificatif, formsemestre ) # Filtrage des justificatifs if with_query: justificatifs_query: Query = _filter_manager(request, justificatifs_query) # Retour des justificatifs en JSON restrict = not current_user.has_permission(Permission.AbsJustifView) data_set: list[dict] = [] for justi in justificatifs_query.all(): data = justi.to_dict(format_api=True, restrict=restrict) data_set.append(data) return data_set @bp.route("/justificatif//create", methods=["POST"]) @api_web_bp.route("/justificatif//create", methods=["POST"]) @bp.route("/justificatif/etudid//create", methods=["POST"]) @api_web_bp.route("/justificatif/etudid//create", methods=["POST"]) # nip @bp.route("/justificatif/nip//create", methods=["POST"]) @api_web_bp.route("/justificatif/nip//create", methods=["POST"]) # ine @bp.route("/justificatif/ine//create", methods=["POST"]) @api_web_bp.route("/justificatif/ine//create", methods=["POST"]) @scodoc @login_required @as_json @permission_required(Permission.AbsChange) def justif_create(etudid: int = None, nip=None, ine=None): """ Création d'un justificatif pour l'é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, "raison":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", ) set_sco_dept(etud.departement.acronym) # 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[dict] = [] success: list[dict] = [] # é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: success.append({"indice": i, "message": obj}) justi.justifier_assiduites() scass.simple_invalidate_cache(data, etud.id) return {"errors": errors, "success": success} def _create_one( data: dict, etud: Identite, ) -> tuple[int, object, Justificatif]: 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.EtatJustificatif.contains(etat): errors.append("param 'etat': invalide") etat: scu.EtatJustificatif = scu.EtatJustificatif.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") 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: 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") # cas 4 : raison raison: str = data.get("raison", None) 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") if errors: err: str = ", ".join(errors) return (404, err, None) # TOUT EST OK try: # On essaye de créer le justificatif nouv_justificatif: Query = Justificatif.create_justificatif( date_debut=deb, date_fin=fin, etat=etat, etudiant=etud, raison=raison, user_id=current_user.id, 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() return ( 200, { "justif_id": nouv_justificatif.id, "couverture": scass.justifies(nouv_justificatif), }, nouv_justificatif, ) except ScoValueError as excp: return (404, excp.args[0], None) @bp.route("/justificatif//edit", methods=["POST"]) @api_web_bp.route("/justificatif//edit", methods=["POST"]) @login_required @scodoc @as_json @permission_required(Permission.AbsChange) def justif_edit(justif_id: int): """ Edition d'un justificatif à partir de son id La requête doit avoir un content type "application/json": { "etat"?: str, "raison"?: str "date_debut"?: str "date_fin"?: str } """ # Récupération du justificatif à modifier justificatif_unique = Justificatif.get_justificatif(justif_id) 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 = scu.EtatJustificatif.get(data.get("etat")) if etat is None: errors.append("param 'etat': invalide") else: justificatif_unique.etat = etat # Cas 2 : raison 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: str = data.get("date_debut", False) if date_debut is not False: if date_debut is None: errors.append("param 'date_debut': manquant") 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: str = data.get("date_fin", False) if date_fin is not False: if date_fin is None: errors.append("param 'date_fin': manquant") fin: datetime = scu.is_iso_formated(date_fin.replace(" ", "+"), convert=True) if fin is None: errors.append("param 'date_fin': format invalide") # 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 # 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") else: justificatif_unique.external_data = external_data 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 if errors: err: str = ", ".join(errors) return json_error(404, err) # Mise à jour du justificatif justificatif_unique.dejustifier_assiduites() db.session.add(justificatif_unique) db.session.commit() Scolog.logdb( method="edit_justificatif", etudid=justificatif_unique.etudiant.id, msg=f"justificatif modif: {justificatif_unique}", ) # 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, "apres": justificatif_unique.justifier_assiduites(), } } # Invalide le cache scass.simple_invalidate_cache(justificatif_unique.to_dict()) return retour @bp.route("/justificatif/delete", methods=["POST"]) @api_web_bp.route("/justificatif/delete", methods=["POST"]) @login_required @scodoc @as_json @permission_required(Permission.AbsChange) def justif_delete(): """ Suppression d'un justificatif à partir de son id Forme des données envoyées : [ , ... ] """ # 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") output = {"errors": [], "success": []} for i, ass in enumerate(justificatifs_list): code, msg = _delete_one(ass) 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(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 try: justificatif_unique = Justificatif.get_justificatif(justif_id) except NotFound: 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()) # On actualise les assiduités justifiées de l'étudiant concerné justificatif_unique.dejustifier_assiduites() # On supprime le justificatif db.session.delete(justificatif_unique) return (200, "OK") # Partie archivage @bp.route("/justificatif//import", methods=["POST"]) @api_web_bp.route("/justificatif//import", methods=["POST"]) @scodoc @login_required @as_json @permission_required(Permission.AbsChange) 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 justificatif_unique = Justificatif.get_justificatif(justif_id) # 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, filename=file.filename, data=file.stream.read(), archive_name=archive_name, user_id=current_user.id, ) # On actualise l'archive du justificatif justificatif_unique.fichier = archive_name db.session.add(justificatif_unique) db.session.commit() return {"filename": fname} except ScoValueError as exc: # Si cela ne fonctionne pas on renvoie une erreur return json_error(404, exc.args[0]) @bp.route("/justificatif//export/", methods=["GET", "POST"]) @api_web_bp.route( "/justificatif//export/", methods=["GET", "POST"] ) @scodoc @login_required @permission_required(Permission.ScoView) def justif_export(justif_id: int | None = None, filename: str | None = None): """ Retourne un fichier d'une archive d'un justificatif. La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif) """ # On récupère le justificatif concerné justificatif_unique = Justificatif.get_justificatif(justif_id) # Vérification des permissions if not ( current_user.has_permission(Permission.AbsJustifView) or justificatif_unique.user_id == current_user.id ): return json_error(401, "non autorisé à voir ce fichier") # 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]) @bp.route("/justificatif//remove", methods=["POST"]) @api_web_bp.route("/justificatif//remove", methods=["POST"]) @scodoc @login_required @as_json @permission_required(Permission.AbsChange) def justif_remove(justif_id: int = None): """ Supression d'un fichier ou d'une archive { "remove": <"all"/"list"> "filenames"?: [ , ... ] } """ # On récupère le dictionnaire data: dict = request.get_json(force=True) # On récupère le justificatif concerné justificatif_unique = Justificatif.get_justificatif(justif_id) # 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, archive_name=archive_name, 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 db.session.add(justificatif_unique) 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"} @bp.route("/justificatif//list", methods=["GET"]) @api_web_bp.route("/justificatif//list", methods=["GET"]) @scodoc @login_required @as_json @permission_required(Permission.ScoView) def justif_list(justif_id: int = None): """ Liste les fichiers du justificatif """ # Récupération du justificatif concerné justificatif_unique = Justificatif.get_justificatif(justif_id) # 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 # Partie justification @bp.route("/justificatif//justifies", methods=["GET"]) @api_web_bp.route("/justificatif//justifies", methods=["GET"]) @scodoc @login_required @as_json @permission_required(Permission.AbsChange) def justif_justifies(justif_id: int = None): """ Liste assiduite_id justifiées par le justificatif """ # On récupère le justificatif concerné justificatif_unique = Justificatif.get_justificatif(justif_id) # 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: Query): """ Retourne les justificatifs entrés filtrés en fonction de la request et du département courant s'il y en a un """ # cas 1 : etat justificatif etat: str = requested.args.get("etat") if etat is not None: justificatifs_query: Query = scass.filter_justificatifs_by_etat( justificatifs_query, etat ) # cas 2 : date de début deb: str = requested.args.get("date_debut", "").replace(" ", "+") deb: datetime = scu.is_iso_formated(deb, True) # cas 3 : date de fin 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( justificatifs_query, user_id ) # cas 5 : formsemestre_id formsemestre_id = requested.args.get("formsemestre_id") if formsemestre_id not in [None, "", -1]: formsemestre: FormSemestre = None try: formsemestre_id = int(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id) justificatifs_query = scass.filter_by_formsemestre( justificatifs_query, Justificatif, formsemestre ) 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() justificatifs_query: Query = justificatifs_query.filter( Justificatif.date_debut >= scu.date_debut_annee_scolaire(annee), Justificatif.date_fin <= scu.date_fin_annee_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: group_id = int(group_id) etudids: list[int] = [etu["etudid"] for etu in get_group_members(group_id)] justificatifs_query = justificatifs_query.filter( Justificatif.etudid.in_(etudids) ) except ValueError: group_id = None # Département if g.scodoc_dept: justificatifs_query = justificatifs_query.join(Identite).filter_by( dept_id=g.scodoc_dept_id ) return justificatifs_query