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