diff --git a/app/api/assiduites.py b/app/api/assiduites.py index f067641f..ec67fd7c 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -40,14 +40,7 @@ def assiduite(assiduite_id: int = None): } """ - query = Assiduite.query.filter_by(id=assiduite_id) - # if g.scodoc_dept: - # query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - assiduite_query = query.first_or_404() - - data = assiduite_query.to_dict() - - return jsonify(_change_etat(data)) + return scu.get_model_api_object(Assiduite, assiduite_id) @bp.route("/assiduites//count", defaults={"with_query": False}) @@ -164,8 +157,8 @@ def assiduites(etudid: int = None, with_query: bool = False): data_set: list[dict] = [] for ass in assiduites_query.all(): - data = ass.to_dict() - data_set.append(_change_etat(data)) + data = ass.to_dict(format_api=True) + data_set.append(data) return jsonify(data_set) @@ -202,8 +195,8 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): data_set: list[dict] = [] for ass in assiduites_query.all(): - data = ass.to_dict() - data_set.append(_change_etat(data)) + data = ass.to_dict(format_api=True) + data_set.append(data) return jsonify(data_set) @@ -307,11 +300,10 @@ def _create_singular( etat = data.get("etat", None) if etat is None: errors.append("param 'etat': manquant") - elif etat not in scu.ETATS_ASSIDUITE: + elif not scu.EtatAssiduite.contains(etat): errors.append("param 'etat': invalide") - data = _change_etat(data, False) - etat = data.get("etat", None) + etat = scu.EtatAssiduite.get(etat) # cas 2 : date_debut date_debut = data.get("date_debut", None) @@ -418,7 +410,7 @@ def _delete_singular(assiduite_id: int, database): @scodoc @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) -def assiduite_cedit(assiduite_id: int): +def assiduite_edit(assiduite_id: int): """ Edition d'une assiduité à partir de son id La requête doit avoir un content type "application/json": @@ -438,11 +430,11 @@ def assiduite_cedit(assiduite_id: int): # Cas 1 : Etat if data.get("etat") is not None: - data = _change_etat(data, False) - if data.get("etat") is None: + etat = scu.EtatAssiduite.get(data.get("etat")) + if etat is None: errors.append("param 'etat': invalide") else: - assiduite_unique.etat = data.get("etat") + assiduite_unique.etat = etat # Cas 2 : Moduleimpl_id moduleimpl_id = data.get("moduleimpl_id", False) @@ -478,13 +470,6 @@ def assiduite_cedit(assiduite_id: int): # -- Utils -- -def _change_etat(data: dict, from_int: bool = True): - """change dans un json la valeur du champs état""" - if from_int: - data["etat"] = scu.ETAT_ASSIDUITE_NAME.get(data["etat"]) - else: - data["etat"] = scu.ETATS_ASSIDUITE.get(data["etat"]) - return data def _count_manager(requested) -> tuple[str, dict]: diff --git a/app/api/justificatif.py b/app/api/justificatif.py index c51b0459..3b025008 100644 --- a/app/api/justificatif.py +++ b/app/api/justificatif.py @@ -5,22 +5,22 @@ ############################################################################## """ScoDoc 9 API : Assiduités """ -import os from datetime import datetime +from flask import g, jsonify, request +from flask_login import login_required + import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app import db - from app.api import api_bp as bp from app.api import api_web_bp -from app.scodoc.sco_exceptions import ScoValueError from app.decorators import permission_required, scodoc from app.models import Identite, Justificatif +from app.models.assiduites import is_period_conflicting from app.scodoc.sco_archives_justificatifs import JustificatifArchiver +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission -from flask import g, jsonify, request -from flask_login import login_required from app.scodoc.sco_utils import json_error @@ -35,9 +35,8 @@ from app.scodoc.sco_utils import json_error # return jsonify("done") # Partie Modèle -# TODO: justificatif @bp.route("/justificatif/") -@api_web_bp.route("/assiduite/") +@api_web_bp.route("/justificatif/") @scodoc @permission_required(Permission.ScoView) def justificatif(justif_id: int = None): @@ -54,19 +53,12 @@ def justificatif(justif_id: int = None): "raison": "une raison", "entry_date": "2022-10-31T08:00+01:00", } + """ - query = Justificatif.query.filter_by(id=justif_id) - if g.scodoc_dept: - query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) - justificatif_unique = query.first_or_404() - - data = justificatif_unique.to_dict() - - return jsonify(_change_etat(data)) + return scu.get_model_api_object(Justificatif, justif_id) -# TODO: justificatifs[-query] @bp.route("/justificatifs/", defaults={"with_query": False}) @bp.route("/justificatifs//query", defaults={"with_query": True}) @api_web_bp.route("/justificatifs/", defaults={"with_query": False}) @@ -110,13 +102,12 @@ def justificatifs(etudid: int = None, with_query: bool = False): data_set: list[dict] = [] for just in justificatifs_query.all(): - data = just.to_dict() - data_set.append(_change_etat(data)) + data = just.to_dict(format_api=True) + data_set.append(data) return jsonify(data_set) -# TODO: justificatif-create @bp.route("/justificatif//create", methods=["POST"]) @api_web_bp.route("/justificatif//create", methods=["POST"]) @scodoc @@ -173,11 +164,10 @@ def _create_singular( etat = data.get("etat", None) if etat is None: errors.append("param 'etat': manquant") - elif etat not in scu.ETATS_JUSTIFICATIF: + elif not scu.EtatJustificatif.contains(etat): errors.append("param 'etat': invalide") - data = _change_etat(data, False) - etat = data.get("etat", None) + etat = scu.EtatJustificatif.get(etat) # cas 2 : date_debut date_debut = data.get("date_debut", None) @@ -224,7 +214,6 @@ def _create_singular( ) -# TODO: justificatif-edit @bp.route("/justificatif//edit", methods=["POST"]) @api_web_bp.route("/justificatif//edit", methods=["POST"]) @login_required @@ -235,9 +224,12 @@ 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 } """ justificatif_unique: Justificatif = Justificatif.query.filter_by( @@ -250,17 +242,58 @@ def justif_edit(justif_id: int): # Cas 1 : Etat if data.get("etat") is not None: - data = _change_etat(data, False) - if data.get("etat") is None: + etat = scu.EtatJustificatif.get(data.get("etat")) + if etat is None: errors.append("param 'etat': invalide") else: - justificatif_unique.etat = data.get("etat") + justificatif_unique.etat = etat # Cas 2 : raison raison = data.get("raison", False) if raison is not False: justificatif_unique.raison = raison + deb, fin = None, None + + # cas 3 : date_debut + date_debut = data.get("date_debut", False) + if date_debut is not False: + if date_debut is None: + errors.append("param 'date_debut': manquant") + deb = scu.is_iso_formated(date_debut, convert=True) + if deb is None: + errors.append("param 'date_debut': format invalide") + + if justificatif_unique.date_fin >= deb: + errors.append("param 'date_debut': date de début située après date de fin ") + + # cas 4 : date_fin + date_fin = data.get("date_fin", False) + if date_fin is not False: + if date_fin is None: + errors.append("param 'date_fin': manquant") + fin = scu.is_iso_formated(date_fin, convert=True) + if fin is None: + errors.append("param 'date_fin': format invalide") + if justificatif_unique.date_debut <= fin: + errors.append("param 'date_fin': date de fin située avant date de début ") + + # Vérification du conflit d'horaire + if (deb is not None) or (fin is not 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 + + justificatifs_list: list[Justificatif] = Justificatif.query.filter_by( + etuid=justificatif_unique.etudid + ).all() + + if is_period_conflicting(deb, fin, justificatifs_list): + errors.append( + "Modification de la plage horaire impossible: conflit avec les autres justificatifs" + ) + justificatif_unique.date_debut = deb + justificatif_unique.date_fin = fin + if errors: err: str = ", ".join(errors) return json_error(404, err) @@ -270,7 +303,6 @@ def justif_edit(justif_id: int): return jsonify({"OK": True}) -# TODO: justificatif-delete @bp.route("/justificatif/delete", methods=["POST"]) @api_web_bp.route("/justificatif/delete", methods=["POST"]) @login_required @@ -312,12 +344,18 @@ def _delete_singular(justif_id: int, database): ).first() if justificatif_unique is None: return (404, "Justificatif non existant") + + archive_name: str = justificatif_unique.fichier + + if archive_name is not None: + archiver: JustificatifArchiver = JustificatifArchiver() + archiver.delete_justificatif(justificatif_unique.etudid, archive_name) + database.session.delete(justificatif_unique) return (200, "OK") # Partie archivage -# TODO: justificatif-import @bp.route("/justificatif/import/", methods=["POST"]) @api_web_bp.route("/justificatif/import/", methods=["POST"]) @scodoc @@ -359,12 +397,11 @@ def justif_import(justif_id: int = None): return jsonify({"response": "imported"}) except ScoValueError as err: - return json_error(404, err.args[1]) + return json_error(404, err.args[0]) -# TODO: justificatif-export -@bp.route("/justificatif/export//", methods=["GET"]) -@api_web_bp.route("/justificatif/export//", methods=["GET"]) +@bp.route("/justificatif/export//", methods=["POST"]) +@api_web_bp.route("/justificatif/export//", methods=["POST"]) @scodoc @login_required @permission_required(Permission.ScoView) @@ -391,10 +428,9 @@ def justif_export(justif_id: int = None, filename: str = None): archive_name, justificatif_unique.etudid, filename ) except ScoValueError as err: - return json_error(404, err.args[1]) + return json_error(404, err.args[0]) -# TODO: justificatif-remove @bp.route("/justificatif/remove/", methods=["POST"]) @api_web_bp.route("/justificatif/remove/", methods=["POST"]) @scodoc @@ -404,7 +440,7 @@ def justif_export(justif_id: int = None, filename: str = None): def justif_remove(justif_id: int = None): """ Supression d'un fichier ou d'une archive - + # TOTALK: Doc, expliquer les noms coté server { "remove": <"all"/"list"> @@ -454,12 +490,11 @@ def justif_remove(justif_id: int = None): db.session.commit() except ScoValueError as err: - return json_error(404, err.args[1]) + return json_error(404, err.args[0]) return jsonify({"response": "removed"}) -# TODO: justificatif-list @bp.route("/justificatif/list/", methods=["GET"]) @api_web_bp.route("/justificatif/list/", methods=["GET"]) @scodoc @@ -492,16 +527,29 @@ def justif_list(justif_id: int = None): # Partie justification # TODO: justificatif-justified +@bp.route("/justificatif/justified/", methods=["GET"]) +@api_web_bp.route("/justificatif/justified/", methods=["GET"]) +@scodoc +@login_required +@permission_required(Permission.ScoView) +# @permission_required(Permission.ScoAssiduiteChange) +def justif_justified(justif_id: int = None): + """ + Liste assiduite_id justifiées par le justificatif + """ + + query = Justificatif.query.filter_by(id=justif_id) + if g.scodoc_dept: + query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) + + justificatif_unique: Justificatif = query.first_or_404() + + assiduites_list: list[int] = scass.justifies(justificatif_unique) + + return jsonify(assiduites_list) # -- Utils -- -def _change_etat(data: dict, from_int: bool = True): - """change dans un json la valeur du champs état""" - if from_int: - data["etat"] = scu.ETAT_JUSTIFICATIF_NAME.get(data["etat"]) - else: - data["etat"] = scu.ETATS_JUSTIFICATIF.get(data["etat"]) - return data def _filter_manager(requested, justificatifs_query): diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 8c1d68da..c533c21a 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -33,10 +33,7 @@ import pandas as pd from app import db from app import models from app.models import ( - DispenseUE, FormSemestre, - FormSemestreInscription, - Identite, Module, ModuleImpl, ModuleUECoef, @@ -218,31 +215,6 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: ) -def load_dispense_ues( - formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns] -) -> set[tuple[int, int]]: - """Construit l'ensemble des - etudids = modimpl_inscr_df.index, # les etudids - ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport - - Résultat: set de (etudid, ue_id). - """ - dispense_ues = set() - ue_sem_by_code = {ue.ue_code: ue for ue in ues} - # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, - # puis filtre sur inscrits et code d'UE UE - for dispense_ue in DispenseUE.query.join( - Identite, FormSemestreInscription - ).filter_by(formsemestre_id=formsemestre.id): - if dispense_ue.etudid in etudids: - # UE dans le semestre avec même code ? - ue = ue_sem_by_code.get(dispense_ue.ue.ue_code) - if ue is not None: - dispense_ues.add((dispense_ue.etudid, ue.id)) - - return dispense_ues - - def compute_ue_moys_apc( sem_cube: np.array, etuds: list, diff --git a/app/comp/res_but.py b/app/comp/res_but.py index cf7f4101..f3f0c97d 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -72,7 +72,7 @@ class ResultatsSemestreBUT(NotesTableCompat): modimpl.module.ue.type != UE_SPORT for modimpl in self.formsemestre.modimpls_sorted ] - self.dispense_ues = moy_ue.load_dispense_ues( + self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set( self.formsemestre, self.modimpl_inscr_df.index, self.ues ) self.etud_moy_ue = moy_ue.compute_ue_moys_apc( diff --git a/app/models/assiduites.py b/app/models/assiduites.py index ede6aacd..ea59c967 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -51,14 +51,18 @@ class Assiduite(db.Model): entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) - def to_dict(self) -> dict: + def to_dict(self, format_api=True) -> dict: + etat = self.etat + + if format_api: + etat = EtatJustificatif.inverse().get(self.etat).name data = { "assiduite_id": self.assiduite_id, "etudid": self.etudid, "moduleimpl_id": self.moduleimpl_id, "date_debut": self.date_debut, "date_fin": self.date_fin, - "etat": self.etat, + "etat": etat, "desc": self.desc, "entry_date": self.entry_date, } @@ -78,17 +82,8 @@ class Assiduite(db.Model): # Vérification de non duplication des périodes assiduites: list[Assiduite] = etud.assiduites.all() - date_debut = localize_datetime(date_debut) - date_fin = localize_datetime(date_fin) - assiduites = [ - ass - for ass in assiduites - if is_period_overlapping( - (date_debut, date_fin), - (ass.date_debut, ass.date_fin), - ) - ] - if len(assiduites) != 0: + assiduites: list[Justificatif] = etud.assiduites.all() + if is_period_conflicting(date_debut, date_fin, assiduites): raise ScoValueError( "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" ) @@ -156,13 +151,20 @@ class Justificatif(db.Model): # Archive_id -> sco_archives_justificatifs.py fichier = db.Column(db.Text()) - def to_dict(self) -> dict: + def to_dict(self, format_api: bool = False) -> dict: + """transformation de l'objet en dictionnaire sérialisable""" + + etat = self.etat + + if format_api: + etat = EtatJustificatif.inverse().get(self.etat).name + data = { "justif_id": self.justif_id, "etudid": self.etudid, "date_debut": self.date_debut, "date_fin": self.date_fin, - "etat": self.etat, + "etat": etat, "raison": self.raison, "fichier": self.fichier, "entry_date": self.entry_date, @@ -181,23 +183,12 @@ class Justificatif(db.Model): """Créer un nouveau justificatif pour l'étudiant""" # Vérification de non duplication des périodes justificatifs: list[Justificatif] = etud.justificatifs.all() - - date_debut = localize_datetime(date_debut) - date_fin = localize_datetime(date_fin) - justificatifs = [ - just - for just in justificatifs - if is_period_overlapping( - (date_debut, date_fin), - (just.date_debut, just.date_fin), - ) - ] - if len(justificatifs) != 0: + if is_period_conflicting(date_debut, date_fin, justificatifs): raise ScoValueError( "Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)" ) - nouv_assiduite = Justificatif( + nouv_justificatif = Justificatif( date_debut=date_debut, date_fin=date_fin, etat=etat, @@ -205,4 +196,28 @@ class Justificatif(db.Model): raison=raison, ) - return nouv_assiduite + return nouv_justificatif + + +def is_period_conflicting( + date_debut: datetime, + date_fin: datetime, + collection: list[Assiduite or Justificatif], +) -> bool: + """ + Vérifie si une date n'entre pas en collision + avec les justificatifs ou assiduites déjà présentes + """ + + date_debut = localize_datetime(date_debut) + date_fin = localize_datetime(date_fin) + unified = [ + uni + for uni in collection + if is_period_overlapping( + (date_debut, date_fin), + (uni.date_debut, uni.date_fin), + ) + ] + + return len(unified) != 0 diff --git a/app/models/ues.py b/app/models/ues.py index 2f14ee74..596e0bef 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -256,12 +256,23 @@ class UniteEns(db.Model): class DispenseUE(db.Model): """Dispense d'UE - Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée + Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas. + La dispense d'UE n'est PAS une validation: + - elle n'est pas affectée par les décisions de jury (pas effacée) + - elle est associée à un formsemestre + - elle ne permet pas la délivrance d'ECTS ou du diplôme. + + On utilise cette dispense et non une "inscription" par souci d'efficacité: + en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours, + la dispense étant une exception. """ - __table_args__ = (db.UniqueConstraint("ue_id", "etudid"),) + __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),) id = db.Column(db.Integer, primary_key=True) + formsemestre_id = formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True + ) ue_id = db.Column( db.Integer, db.ForeignKey(UniteEns.id, ondelete="CASCADE"), @@ -280,3 +291,25 @@ class DispenseUE(db.Model): def __repr__(self) -> str: return f"""<{self.__class__.__name__} {self.id} etud={ repr(self.etud)} ue={repr(self.ue)}>""" + + @classmethod + def load_formsemestre_dispense_ues_set( + cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns] + ) -> set[tuple[int, int]]: + """Construit l'ensemble des + etudids = modimpl_inscr_df.index, # les etudids + ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport + + Résultat: set de (etudid, ue_id). + """ + # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, + # puis filtre sur inscrits et ues + ue_ids = {ue.id for ue in ues} + dispense_ues = { + (dispense_ue.etudid, dispense_ue.ue_id) + for dispense_ue in DispenseUE.query.filter_by( + formsemestre_id=formsemestre.id + ) + if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids + } + return dispense_ues diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py index 8439d1df..e48d7e92 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -89,7 +89,7 @@ class BaseArchiver(object): self.archive_type = archive_type self.initialized = False self.root = None - self.dept_id = getattr(g, "scodoc_dept_id") + self.dept_id = None def set_dept_id(self, dept_id: int): "set dept" @@ -115,6 +115,8 @@ class BaseArchiver(object): finally: scu.GSL.release() self.initialized = True + if self.dept_id is None: + self.dept_id = getattr(g, "scodoc_dept_id") def get_obj_dir(self, oid: int): """ diff --git a/app/scodoc/sco_archives_justificatifs.py b/app/scodoc/sco_archives_justificatifs.py index e64d788a..7202f5e1 100644 --- a/app/scodoc/sco_archives_justificatifs.py +++ b/app/scodoc/sco_archives_justificatifs.py @@ -19,9 +19,6 @@ class JustificatifArchiver(BaseArchiver): ├── [_description.txt] └── [] - - TODO: - - Faire fonction suppression fichier unique dans archive """ def __init__(self): @@ -38,6 +35,7 @@ class JustificatifArchiver(BaseArchiver): """ Ajoute un fichier dans une archive "justificatif" pour l'etudid donné Retourne l'archive_name utilisé + TODO: renvoie archive_name + filename """ self._set_dept(etudid) if archive_name is None: @@ -104,9 +102,8 @@ class JustificatifArchiver(BaseArchiver): ) def _set_dept(self, etudid: int): - if g.scodoc_dept is None or g.scodoc_dept_id is None: - etud: Identite = Identite.query.filter_by(id=etudid).first() - dept: Departement = Departement.query.filter_by(id=etud.dept_id).first() - - g.scodoc_dept = dept.acronym - g.scodoc_dept_id = dept.id + """ + Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant + """ + etud: Identite = Identite.query.filter_by(id=etudid).first() + self.set_dept_id(etud.dept_id) diff --git a/app/scodoc/sco_assiduites.py b/app/scodoc/sco_assiduites.py index c216184e..edba0f10 100644 --- a/app/scodoc/sco_assiduites.py +++ b/app/scodoc/sco_assiduites.py @@ -85,7 +85,7 @@ def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite: Filtrage d'une collection d'assiduites en fonction de leur état """ etats: list[str] = list(etat.split(",")) - etats = [scu.ETATS_ASSIDUITE.get(e, -1) for e in etats] + etats = [scu.EtatAssiduite.get(e, -1) for e in etats] return assiduites.filter(Assiduite.etat.in_(etats)) @@ -117,7 +117,7 @@ def filter_justificatifs_by_etat( Filtrage d'une collection de justificatifs en fonction de leur état """ etats: list[str] = list(etat.split(",")) - etats = [scu.ETATS_JUSTIFICATIF.get(e, -1) for e in etats] + etats = [scu.EtatJustificatif.get(e, -1) for e in etats] return justificatifs.filter(Justificatif.etat.in_(etats)) @@ -172,3 +172,31 @@ def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemest Assiduite.date_debut >= formsemestre.date_debut ) return assiduites_query.filter(Assiduite.date_fin <= formsemestre.date_fin) + + +def justifies(justi: Justificatif) -> list[int]: + """ + Retourne la liste des assiduite_id qui sont justifié par la justification + Une assiduité est justifiée si elle est STRICTEMENT comprise dans la plage du justificatif + et que l'état du justificatif est "validé" + """ + + justified: list[int] = [] + + if justi.etat != scu.EtatJustificatif.VALIDE: + return justified + + assiduites_query: Assiduite = Assiduite.query.join( + Justificatif, Assiduite.etudid == Justificatif.etudid + ).filter(Assiduite.etat != scu.EtatAssiduite.PRESENT) + + assiduites_query = filter_assiduites_by_date( + assiduites_query, justi.date_debut, True + ) + assiduites_query = filter_assiduites_by_date( + assiduites_query, justi.date_fin, False + ) + + justified = [assi.id for assi in assiduites_query.all()] + + return justified diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 134bcd1a..b99af2cc 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -36,7 +36,7 @@ from flask_login import current_user from app import db, log -from app.models import ModuleImpl, ScolarNews +from app.models import Evaluation, ModuleImpl, ScolarNews from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 2edd3655..679ede4e 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -93,7 +93,7 @@ _formsemestreEditor = ndb.EditableTable( ) -def get_formsemestre(formsemestre_id, raise_soft_exc=False): +def get_formsemestre(formsemestre_id: int): "list ONE formsemestre" if formsemestre_id is None: raise ValueError("get_formsemestre: id manquant") @@ -105,10 +105,8 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False): sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id}) if not sems: log(f"get_formsemestre: invalid formsemestre_id ({formsemestre_id})") - if raise_soft_exc: - raise ScoValueError(f"semestre {formsemestre_id} inconnu !") - else: - raise ValueError(f"semestre {formsemestre_id} inconnu !") + raise ScoValueError(f"semestre {formsemestre_id} inconnu !") + g.stored_get_formsemestre[formsemestre_id] = sems[0] return sems[0] diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 16ed01c7..210d1e76 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1629,7 +1629,9 @@ def do_formsemestre_delete(formsemestre_id): req = """DELETE FROM notes_formsemestre_etapes WHERE formsemestre_id=%(formsemestre_id)s""" cursor.execute(req, {"formsemestre_id": formsemestre_id}) - + # --- Dispenses d'UE + req = """DELETE FROM "dispenseUE" WHERE formsemestre_id=%(formsemestre_id)s""" + cursor.execute(req, {"formsemestre_id": formsemestre_id}) # --- Destruction du semestre sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 40224408..0e699868 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -523,7 +523,8 @@ def retreive_formsemestre_from_request() -> int: # Element HTML decrivant un semestre (barre de menu et infos) def formsemestre_page_title(formsemestre_id=None): """Element HTML decrivant un semestre (barre de menu et infos) - Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group) + Cherche dans la requete si un semestre est défini + via (formsemestre_id ou moduleimpl ou evaluation ou group) """ formsemestre_id = ( formsemestre_id @@ -540,15 +541,13 @@ def formsemestre_page_title(formsemestre_id=None): return "" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - h = render_template( + return render_template( "formsemestre_page_title.j2", formsemestre=formsemestre, scu=scu, sem_menu_bar=formsemestre_status_menubar(formsemestre), ) - return h - def fill_formsemestre(sem): """Add some useful fields to help display formsemestres""" @@ -768,8 +767,7 @@ def formsemestre_description_table( caption=title, html_caption=title, html_class="table_leftalign formsemestre_description", - base_url="%s?formsemestre_id=%s&with_evals=%s" - % (request.base_url, formsemestre_id, with_evals), + base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}", page_title=title, html_title=html_sco_header.html_sem_header( "Description du semestre", with_page_header=False @@ -923,7 +921,7 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): f"""

Ajouter une partition

""" @@ -980,14 +978,14 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None ), f"""') H.append( - f"""""" - ) - H.append( - f""" +
Formation: - {formation.titre} """, ] if sem.semestre_id >= 0: - H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id)) + H.append(f", {parcours.SESSION_NAME} {sem.semestre_id}") if sem.modalite: H.append(f" en {sem.modalite}") if sem.etapes: @@ -1091,7 +1089,8 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True): elif datetime.date.today() > formsemestre.date_fin: if formsemestre.etat: H.append( - """semestre du passé non verrouillé""" + """semestre terminé mais non verrouillé""" ) else: H.append( @@ -1101,7 +1100,8 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True): if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id): H.append( - """
Toutes évaluations (même incomplètes) visibles
""" + """
Toutes évaluations (même incomplètes) visibles
""" ) if nt.expr_diagnostics: @@ -1215,6 +1215,11 @@ def formsemestre_tableau_modules( prev_ue_id = None for modimpl in modimpls: mod: Module = Module.query.get(modimpl["module_id"]) + moduleimpl_status_url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl["moduleimpl_id"], + ) mod_descr = "Module " + (mod.titre or "") if mod.is_apc(): coef_descr = ", ".join( @@ -1240,7 +1245,7 @@ def formsemestre_tableau_modules( prev_ue_id = ue["ue_id"] titre = ue["titre"] if use_ue_coefs: - titre += " (coef. %s)" % (ue["coefficient"] or 0.0) + titre += f""" (coef. {ue["coefficient"] or 0.0})""" H.append( f"""
{ue["acronyme"]} @@ -1280,23 +1285,18 @@ def formsemestre_tableau_modules( H.append(f'
{mod.code}{mod.abbrev or mod.titre or ""} + f""" + {mod.code}{mod.abbrev or mod.titre or ""} {len(mod_inscrits)} - { sco_users.user_info(modimpl["responsable_id"])["prenomnom"] } + { + sco_users.user_info(modimpl["responsable_id"])["prenomnom"] + } """ @@ -1331,18 +1331,21 @@ def formsemestre_tableau_modules( ) if nb_evals != 0: H.append( - '%s prévues, %s ok' - % (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"]) + f"""{nb_evals} prévues, + {etat["nb_evals_completes"]} ok""" ) if etat["nb_evals_en_cours"] > 0: H.append( - ', %s en cours' - % (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"]) + f""", { + etat["nb_evals_en_cours"] + } en cours""" ) if etat["attente"]: H.append( - ' [en attente]' - % modimpl["moduleimpl_id"] + f""" [en attente]""" ) elif mod.module_type == ModuleType.MALUS: nb_malus_notes = sum( @@ -1352,10 +1355,10 @@ def formsemestre_tableau_modules( ] ) H.append( - """ - malus (%d notes) + f""" + malus + ({nb_malus_notes} notes) """ - % (modimpl["moduleimpl_id"], nb_malus_notes) ) else: raise ValueError(f"Invalid module_type {mod.module_type}") # a bug diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index de16785c..a3959006 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -300,9 +300,7 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud cnx = ndb.GetDBConnexion() group = get_group(group_id) - sem = sco_formsemestre.get_formsemestre( - group["formsemestre_id"], raise_soft_exc=True - ) + sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"]) members = get_group_members(group_id, etat=etat) # add human readable description of state: diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py index 2bd637d7..c0a72952 100644 --- a/app/scodoc/sco_permissions_check.py +++ b/app/scodoc/sco_permissions_check.py @@ -7,7 +7,7 @@ from flask import g from flask_login import current_user from app.auth.models import User - +from app.models import FormSemestre import app.scodoc.notesdb as ndb from app.scodoc.sco_permissions import Permission from app.scodoc import html_sco_header @@ -164,18 +164,14 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl return True, "" -def can_change_groups(formsemestre_id): +def can_change_groups(formsemestre_id: int) -> bool: "Vrai si l'utilisateur peut changer les groupes dans ce semestre" - from app.scodoc import sco_formsemestre - - sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) - if not sem["etat"]: + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.etat: return False # semestre verrouillé if current_user.has_permission(Permission.ScoEtudChangeGroups): - return True # admin, chef dept - if current_user.id in sem["responsables"]: - return True - return False + return True # typiquement admin, chef dept + return formsemestre.est_responsable(current_user) def can_handle_passwd(user: User, allow_admindepts=False) -> bool: diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 69a115d8..a4edfb36 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -32,7 +32,7 @@ import base64 import bisect import copy import datetime -from enum import IntEnum +from enum import IntEnum, Enum import io import json from hashlib import md5 @@ -50,17 +50,17 @@ from PIL import Image as PILImage import pydot import requests +import dateutil.parser as dtparser import flask from flask import g, request from flask import flash, url_for, make_response, jsonify from werkzeug.http import HTTP_STATUS_CODES from config import Config -from app import log +from app import log, db from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL from app.scodoc import sco_xml -from app.scodoc.intervals import intervalmap import sco_version @@ -88,7 +88,43 @@ ETATS_INSCRIPTION = { } -class EtatAssiduite(IntEnum): +def get_model_api_object(model_cls: db.Model, model_id: int): + from app.models import Identite + + query = model_cls.query.filter_by(id=model_id) + if g.scodoc_dept: + query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) + unique: model_cls = query.first_or_404() + + return jsonify(unique.to_dict(format_api=True)) + + +class BiDirectionalEnum(Enum): + """Permet la recherche inverse d'un enum + Condition : les clés et les valeurs doivent être uniques + les clés doivent être en MAJUSCULES + """ + + @classmethod + def contains(cls, attr: str): + return attr.upper() in cls._member_names_ + + @classmethod + def get(cls, attr: str, default: any = None): + val = None + try: + val = cls[attr.upper()] + except (KeyError, AttributeError): + val = default + return val + + @classmethod + def inverse(cls): + """Retourne un dictionnaire représentant la map inverse de l'Enum""" + return cls._value2member_map_ + + +class EtatAssiduite(int, BiDirectionalEnum): """Code des états d'assiduité""" # Stockés en BD ne pas modifier @@ -110,7 +146,7 @@ ETATS_ASSIDUITE = { } -class EtatJustificatif(IntEnum): +class EtatJustificatif(int, BiDirectionalEnum): """Code des états des justificatifs""" # Stockés en BD ne pas modifier @@ -121,21 +157,6 @@ class EtatJustificatif(IntEnum): MODIFIE = 3 -ETAT_JUSTIFICATIF_NAME = { - EtatJustificatif.VALIDE: "validé", - EtatJustificatif.NON_VALIDE: "non validé", - EtatJustificatif.ATTENTE: "en attente", - EtatJustificatif.MODIFIE: "modifié", -} - -ETATS_JUSTIFICATIF = { - "validé": EtatJustificatif.VALIDE, - "non vaidé": EtatJustificatif.NON_VALIDE, - "en attente": EtatJustificatif.ATTENTE, - "modifié": EtatJustificatif.MODIFIE, -} - - def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None: """ Vérifie si une date est au format iso @@ -147,7 +168,6 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No Retourne None sinon """ - import dateutil.parser as dtparser try: date: datetime.datetime = dtparser.isoparse(date) diff --git a/app/views/notes.py b/app/views/notes.py index b5b65bb4..d5b9bca9 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1621,10 +1621,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): ue = UniteEns.query.get_or_404(ue_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if ue.formation.is_apc(): - if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0: - disp = DispenseUE(ue_id=ue_id, etudid=etudid) + if ( + DispenseUE.query.filter_by( + formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id + ).count() + == 0 + ): + disp = DispenseUE( + formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid + ) db.session.add(disp) db.session.commit() + log(f"etud_desinscrit_ue {etud} {ue}") + Scolog.logdb( + method="etud_desinscrit_ue", + etudid=etud.id, + msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}", + commit=True, + ) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic( diff --git a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py index 7b57ccbf..f41044e2 100644 --- a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py +++ b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py @@ -10,58 +10,86 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'dbcf2175e87f' -down_revision = '5c7b208355df' +revision = "dbcf2175e87f" +down_revision = "5c7b208355df" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('justificatifs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('etudid', sa.Integer(), nullable=False), - sa.Column('etat', sa.Integer(), nullable=False), - sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('raison', sa.Text(), nullable=True), - sa.Column('fichier', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "justificatifs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date_debut", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "date_fin", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("etat", sa.Integer(), nullable=False), + sa.Column( + "entry_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("raison", sa.Text(), nullable=True), + sa.Column("fichier", sa.Text(), nullable=True), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_justificatifs_etudid'), 'justificatifs', ['etudid'], unique=False) - op.create_table('assiduites', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('moduleimpl_id', sa.Integer(), nullable=True), - sa.Column('etudid', sa.Integer(), nullable=False), - sa.Column('etat', sa.Integer(), nullable=False), - sa.Column('desc', sa.Text(), nullable=True), - sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['moduleimpl_id'], ['notes_moduleimpl.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_justificatifs_etudid"), "justificatifs", ["etudid"], unique=False + ) + op.create_table( + "assiduites", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date_debut", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "date_fin", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("moduleimpl_id", sa.Integer(), nullable=True), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("etat", sa.Integer(), nullable=False), + sa.Column("desc", sa.Text(), nullable=True), + sa.Column( + "entry_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["moduleimpl_id"], ["notes_moduleimpl.id"], ondelete="SET NULL" + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_assiduites_etudid"), "assiduites", ["etudid"], unique=False ) - op.create_index(op.f('ix_assiduites_etudid'), 'assiduites', ['etudid'], unique=False) - op.drop_constraint('dispenseUE_formsemestre_id_ue_id_etudid_key', 'dispenseUE', type_='unique') - op.drop_index('ix_dispenseUE_formsemestre_id', table_name='dispenseUE') - op.create_unique_constraint(None, 'dispenseUE', ['ue_id', 'etudid']) - op.drop_constraint('dispenseUE_formsemestre_id_fkey', 'dispenseUE', type_='foreignkey') - op.drop_column('dispenseUE', 'formsemestre_id') # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('dispenseUE', sa.Column('formsemestre_id', sa.INTEGER(), autoincrement=False, nullable=True)) - op.create_foreign_key('dispenseUE_formsemestre_id_fkey', 'dispenseUE', 'notes_formsemestre', ['formsemestre_id'], ['id']) - op.drop_constraint(None, 'dispenseUE', type_='unique') - op.create_index('ix_dispenseUE_formsemestre_id', 'dispenseUE', ['formsemestre_id'], unique=False) - op.create_unique_constraint('dispenseUE_formsemestre_id_ue_id_etudid_key', 'dispenseUE', ['formsemestre_id', 'ue_id', 'etudid']) - op.drop_index(op.f('ix_assiduites_etudid'), table_name='assiduites') - op.drop_table('assiduites') - op.drop_index(op.f('ix_justificatifs_etudid'), table_name='justificatifs') - op.drop_table('justificatifs') + op.drop_index(op.f("ix_assiduites_etudid"), table_name="assiduites") + op.drop_table("assiduites") + op.drop_index(op.f("ix_justificatifs_etudid"), table_name="justificatifs") + op.drop_table("justificatifs") # ### end Alembic commands ### diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 875f767b..8e5b16e3 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -1,5 +1,5 @@ """ -Test de l'api Assiduité +Test de l'api justificatif Ecrit par HARTMANN Matthias @@ -121,7 +121,7 @@ def test_route_create(api_headers): # -== Unique ==- # Bon fonctionnement - data = create_data("validé", "01") + data = create_data("valide", "01") res = POST_JSON(f"/justificatif/{ETUDID}/create", [data], api_headers) check_fields(res, BATCH_FIELD) @@ -129,7 +129,7 @@ def test_route_create(api_headers): TO_REMOVE.append(res["success"]["0"]["justif_id"]) - data2 = create_data("modifié", "02", "raison") + data2 = create_data("modifie", "02", "raison") res = POST_JSON(f"/justificatif/{ETUDID}/create", [data2], api_headers) check_fields(res, BATCH_FIELD) assert len(res["success"]) == 1 @@ -160,7 +160,7 @@ def test_route_create(api_headers): # Bon Fonctionnement - etats = ["validé", "modifé", "non validé", "en attente"] + etats = ["valide", "modifie", "non_valide", "attente"] data = [ create_data(etats[d % 4], 10 + d, "raison" if d % 2 else None) for d in range(randint(3, 5)) @@ -175,10 +175,10 @@ def test_route_create(api_headers): # Mauvais Fonctionnement data2 = [ - create_data("modifié", "01"), + create_data("modifie", "01"), create_data(None, "25"), create_data("blabla", 26), - create_data("validé", 32), + create_data("valide", 32), ] res = POST_JSON(f"/justificatif/{ETUDID}/create", data2, api_headers) @@ -201,7 +201,7 @@ def test_route_edit(api_headers): # Bon fonctionnement - data = {"etat": "modifié", "raison": "test"} + data = {"etat": "modifie", "raison": "test"} res = POST_JSON(f"/justificatif/{TO_REMOVE[0]}/edit", data, api_headers) assert res == {"OK": True} @@ -209,6 +209,8 @@ def test_route_edit(api_headers): res = POST_JSON(f"/justificatif/{TO_REMOVE[1]}/edit", data, api_headers) assert res == {"OK": True} + # TODO: Modification date deb / fin + # Mauvais fonctionnement check_failure_post(f"/justificatif/{FAUX}/edit", api_headers, data) @@ -288,7 +290,7 @@ def send_file(justif_id: int, filename: str, headers): def check_failure_send( justif_id: int, headers, - filename: str = "/opt/scodoc/tests/api/test_api_justificatif.txt", + filename: str = "tests/api/test_api_justificatif.txt", err: str = None, ): try: @@ -305,12 +307,13 @@ def test_import_justificatif(api_headers): # Bon fonctionnement - filename: str = "/opt/scodoc/tests/api/test_api_justificatif.txt" + filename: str = "tests/api/test_api_justificatif.txt" + resp: dict = send_file(1, filename, api_headers) assert "response" in resp assert resp["response"] == "imported" - filename: str = "/opt/scodoc/tests/api/test_api_justificatif2.txt" + filename: str = "tests/api/test_api_justificatif2.txt" resp: dict = send_file(1, filename, api_headers) assert "response" in resp assert resp["response"] == "imported" @@ -339,13 +342,32 @@ def test_list_justificatifs(api_headers): check_failure_get(f"/justificatif/list/{FAUX}", api_headers) +def post_export(id: int, fname: str, api_headers): + url: str = API_URL + f"/justificatif/export/{id}/{fname}" + res = requests.post(url, headers=api_headers) + return res + + +def test_export(api_headers): + # Bon fonctionnement + + assert post_export(1, "test_api_justificatif.txt", api_headers).status_code == 200 + + # Mauvais fonctionnement + assert ( + post_export(FAUX, "test_api_justificatif.txt", api_headers).status_code == 404 + ) + assert post_export(1, "blabla.txt", api_headers).status_code == 404 + assert post_export(2, "blabla.txt", api_headers).status_code == 404 + + def test_remove_justificatif(api_headers): # Bon fonctionnement - filename: str = "/opt/scodoc/tests/api/test_api_justificatif.txt" + filename: str = "tests/api/test_api_justificatif.txt" send_file(2, filename, api_headers) - filename: str = "/opt/scodoc/tests/api/test_api_justificatif2.txt" + filename: str = "tests/api/test_api_justificatif2.txt" send_file(2, filename, api_headers) res: dict = POST_JSON("/justificatif/remove/1", {"remove": "all"}, api_headers) @@ -372,3 +394,15 @@ def test_remove_justificatif(api_headers): check_failure_post("/justificatif/remove/2", api_headers, {}) check_failure_post(f"/justificatif/remove/{FAUX}", api_headers, {"remove": "all"}) + check_failure_post("/justificatif/remove/1", api_headers, {"remove": "all"}) + + +def test_justified(api_headers): + # Bon fonctionnement + + res: list = GET("/justificatif/justified/1", api_headers) + assert isinstance(res, list) + + # Mauvais fonctionnement + + check_failure_get(f"/justificatif/justified/{FAUX}", api_headers) diff --git a/tests/unit/test_assiduites.py b/tests/unit/test_assiduites.py index 42b80b3f..50f7097a 100644 --- a/tests/unit/test_assiduites.py +++ b/tests/unit/test_assiduites.py @@ -14,7 +14,7 @@ from app import db from app.scodoc import sco_formsemestre import app.scodoc.sco_assiduites as scass -from app.models import Assiduite, Identite, FormSemestre, ModuleImpl +from app.models import Assiduite, Justificatif, Identite, FormSemestre, ModuleImpl from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu @@ -115,13 +115,226 @@ def test_general(test_client): etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first() ajouter_assiduites(etuds, moduleimpls, etud_faux) - verifier_comptage_et_filtrage( + justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0]) + verifier_comptage_et_filtrage_assiduites( etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3) ) - editer_supprimer_assiduiter(etuds, moduleimpls) + verifier_filtrage_justificatifs(etuds[0], justificatifs) + + editer_supprimer_assiduites(etuds, moduleimpls) + editer_supprimer_justificatif(etuds[0]) -def editer_supprimer_assiduiter(etuds: list[Identite], moduleimpls: list[int]): +def ajouter_justificatifs(etud): + + obj_justificatifs = [ + { + "etat": scu.EtatJustificatif.ATTENTE, + "deb": "2022-09-03T08:00+01:00", + "fin": "2022-09-03T09:59:59+01:00", + "raison": None, + }, + { + "etat": scu.EtatJustificatif.VALIDE, + "deb": "2023-01-03T07:00+01:00", + "fin": "2023-01-03T11:00+01:00", + "raison": None, + }, + { + "etat": scu.EtatJustificatif.VALIDE, + "deb": "2022-09-03T10:00:00+01:00", + "fin": "2022-09-03T12:00+01:00", + "raison": None, + }, + { + "etat": scu.EtatJustificatif.NON_VALIDE, + "deb": "2022-09-03T14:00:00+01:00", + "fin": "2022-09-03T15:00+01:00", + "raison": "Description", + }, + { + "etat": scu.EtatJustificatif.MODIFIE, + "deb": "2023-01-03T11:30+01:00", + "fin": "2023-01-03T12:00+01:00", + "raison": None, + }, + ] + + justificatifs = [ + Justificatif.create_justificatif( + etud, + scu.is_iso_formated(just["deb"], True), + scu.is_iso_formated(just["fin"], True), + just["etat"], + just["raison"], + ) + for just in obj_justificatifs + ] + # Vérification de la création des justificatifs + assert [ + justi for justi in justificatifs if not isinstance(justi, Justificatif) + ] == [], "La création des justificatifs de base n'est pas OK" + + # Vérification de la gestion des erreurs + + test_assiduite = { + "etat": scu.EtatJustificatif.ATTENTE, + "deb": "2023-01-03T11:00:01+01:00", + "fin": "2023-01-03T12:00+01:00", + "raison": "Description", + } + + try: + Justificatif.create_justificatif( + etud, + scu.is_iso_formated(test_assiduite["deb"], True), + scu.is_iso_formated(test_assiduite["fin"], True), + test_assiduite["etat"], + test_assiduite["raison"], + ) + except ScoValueError as excp: + assert ( + excp.args[0] + == "Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)" + ) + return justificatifs + + +def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justificatif]): + """ + - vérifier le filtrage des justificatifs (etat, debut, fin) + """ + + # Vérification du filtrage classique + + # Etat + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "valide").count() == 2 + ), "Filtrage de l'état 'valide' mauvais" + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "attente").count() == 1 + ), f"Filtrage de l'état 'attente' mauvais" + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "modifie").count() == 1 + ), "Filtrage de l'état 'modifie' mauvais" + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "non_valide").count() + == 1 + ), "Filtrage de l'état 'non_valide' mauvais" + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "valide,modifie").count() + == 3 + ), "Filtrage de l'état 'valide,modifie' mauvais" + assert ( + scass.filter_justificatifs_by_etat( + etud.justificatifs, "valide,modifie,attente" + ).count() + == 4 + ), "Filtrage de l'état 'valide,modifie,attente' mauvais" + assert ( + scass.filter_justificatifs_by_etat( + etud.justificatifs, "valide,modifie,attente,non_valide" + ).count() + == 5 + ), "Filtrage de l'état 'valide,modifie,attente,_non_valide' mauvais" + + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "autre").count() == 0 + ), "Filtrage de l'état 'autre' mauvais" + + # Date début + date = scu.localize_datetime("2022-09-01T10:00+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() + == 5 + ), "Filtrage 'Date début' mauvais 1" + date = scu.localize_datetime("2022-09-03T08:00:00+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() + == 5 + ), "Filtrage 'Date début' mauvais 2" + date = scu.localize_datetime("2022-09-03T09:00:00+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() + == 4 + ), "Filtrage 'Date début' mauvais 3" + date = scu.localize_datetime("2022-09-03T09:00:02+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count() + == 4 + ), "Filtrage 'Date début' mauvais 4" + + # Date fin + date = scu.localize_datetime("2022-09-01T10:00+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + == 0 + ), "Filtrage 'Date fin' mauvais 1" + date = scu.localize_datetime("2022-09-03T10:00:00+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + == 1 + ), "Filtrage 'Date fin' mauvais 2" + date = scu.localize_datetime("2022-09-03T10:00:01+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + == 1 + ), "Filtrage 'Date fin' mauvais 3" + date = scu.localize_datetime("2023-01-04T13:00:01+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + == 5 + ), "Filtrage 'Date fin' mauvais 4" + date = scu.localize_datetime("2023-01-03T11:00:01+01:00") + assert ( + scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count() + == 4 + ), "Filtrage 'Date fin' mauvais 5" + + # Justifications des assiduites + + assert len(scass.justifies(justificatifs[2])) == 1, "Justifications mauvais" + assert len(scass.justifies(justificatifs[0])) == 0, f"Justifications mauvais" + + +def editer_supprimer_justificatif(etud: Identite): + """ + Troisième Partie: + - Vérification de l'édition des justificatifs + - Vérification de la suppression des justificatifs + """ + + justi: Justificatif = etud.justificatifs.first() + + # Modification de l'état + justi.etat = scu.EtatJustificatif.MODIFIE + db.session.add(justi) + # Modification du moduleimpl + justi.date_debut = scu.localize_datetime("2023-02-03T11:00:01+01:00") + justi.fin = scu.localize_datetime("2023-02-03T12:00:01+01:00") + db.session.add(justi) + db.session.commit() + + # Vérification du changement + assert ( + scass.filter_justificatifs_by_etat(etud.justificatifs, "modifie").count() == 2 + ), "Edition de justificatif mauvais" + assert ( + scass.filter_justificatifs_by_date( + etud.justificatifs, scu.localize_datetime("2023-02-03T11:00:00+01:00") + ).count() + == 1 + ), "Edition de justificatif mauvais" + + # Supression d'une assiduité + + db.session.delete(justi) + db.session.commit() + + assert etud.justificatifs.count() == 4, "Supression de justificatif mauvais" + + +def editer_supprimer_assiduites(etuds: list[Identite], moduleimpls: list[int]): """ Troisième Partie: - Vérification de l'édition des assiduitées @@ -217,8 +430,8 @@ def ajouter_assiduites( assiduites = [ Assiduite.create_assiduite( etud, - ass["deb"], - ass["fin"], + scu.is_iso_formated(ass["deb"], True), + scu.is_iso_formated(ass["fin"], True), ass["etat"], ass["moduleimpl"], ass["desc"], @@ -244,8 +457,8 @@ def ajouter_assiduites( try: Assiduite.create_assiduite( etuds[0], - test_assiduite["deb"], - test_assiduite["fin"], + scu.is_iso_formated(test_assiduite["deb"], True), + scu.is_iso_formated(test_assiduite["fin"], True), test_assiduite["etat"], test_assiduite["moduleimpl"], test_assiduite["desc"], @@ -258,8 +471,8 @@ def ajouter_assiduites( try: Assiduite.create_assiduite( etud_faux, - test_assiduite["deb"], - test_assiduite["fin"], + scu.is_iso_formated(test_assiduite["deb"], True), + scu.is_iso_formated(test_assiduite["fin"], True), test_assiduite["etat"], test_assiduite["moduleimpl"], test_assiduite["desc"], @@ -268,7 +481,7 @@ def ajouter_assiduites( assert excp.args[0] == "L'étudiant n'est pas inscrit au moduleimpl" -def verifier_comptage_et_filtrage( +def verifier_comptage_et_filtrage_assiduites( etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[int] ): """ diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py index 64a10958..3c38a6e7 100644 --- a/tests/unit/test_but_ues.py +++ b/tests/unit/test_but_ues.py @@ -123,7 +123,13 @@ def test_ue_moy(test_client): modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted ] etud_moy_ue = moy_ue.compute_ue_moys_apc( - sem_cube, etuds, modimpls, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask + sem_cube, + etuds, + modimpls, + modimpl_inscr_df, + modimpl_coefs_df, + modimpl_mask, + set(), ) assert etud_moy_ue[ue1.id][etudid] == n1 assert etud_moy_ue[ue2.id][etudid] == n1