Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
9 changed files with 725 additions and 277 deletions
Showing only changes of commit bb4a427207 - Show all commits

View File

@ -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) etud: Identite = tools.get_etud(etudid, nip, ine)
# Vérification que l'étudiant existe
if etud is None: if etud is None:
return json_error( return json_error(
404, 404,
message="étudiant inconnu", message="étudiant inconnu",
) )
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
filtered: dict[str, object] = {} filtered: dict[str, object] = {}
# la métrique du comptage (all, demi, heure, journee)
metric: str = "all" metric: str = "all"
# Si la requête a des paramètres
if with_query: if with_query:
metric, filtered = _count_manager(request) 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) # Récupération de l'étudiant
# if g.scodoc_dept:
# query = query.filter_by(dept_id=g.scodoc_dept_id)
# etud: Identite = query.first_or_404(etudid)
etud: Identite = tools.get_etud(etudid, nip, ine) etud: Identite = tools.get_etud(etudid, nip, ine)
if etud is None: if etud is None:
@ -266,15 +261,23 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
404, 404,
message="étudiant inconnu", message="étudiant inconnu",
) )
# Récupération des assiduités de l'étudiant
assiduites_query: Query = etud.assiduites assiduites_query: Query = etud.assiduites
# Filtrage des assiduités en fonction des paramètres de la requête
if with_query: if with_query:
assiduites_query = _filter_manager(request, assiduites_query) assiduites_query = _filter_manager(request, assiduites_query)
# Préparation de la réponse json
data_set: list[dict] = [] data_set: list[dict] = []
for ass in assiduites_query.all(): for ass in assiduites_query.all():
# conversion Assiduite -> Dict
data = ass.to_dict(format_api=True) data = ass.to_dict(format_api=True)
# Ajout des justificatifs (ou non dépendamment de la requête)
data = _with_justifs(data) data = _with_justifs(data)
# Ajout de l'assiduité dans la liste de retour
data_set.append(data) data_set.append(data)
return data_set 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 = request.args.get("etudids", "")
etuds = etuds.split(",") etuds = etuds.split(",")
try: try:
@ -333,6 +337,7 @@ def assiduites_group(with_query: bool = False):
except ValueError: except ValueError:
return json_error(404, "Le champs etudids n'est pas correctement formé") 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)) query = Identite.query.filter(Identite.id.in_(etuds))
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
@ -342,15 +347,21 @@ def assiduites_group(with_query: bool = False):
404, 404,
"Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.", "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)) 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: if with_query:
assiduites_query = _filter_manager(request, assiduites_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} data_set: dict[list[dict]] = {str(key): [] for key in etuds}
for ass in assiduites_query.all(): for ass in assiduites_query.all():
data = ass.to_dict(format_api=True) data = ass.to_dict(format_api=True)
data = _with_justifs(data) data = _with_justifs(data)
# Ajout de l'assiduité dans la liste du bon étudiant
data_set.get(str(data["etudid"])).append(data) data_set.get(str(data["etudid"])).append(data)
return data_set return data_set
@ -375,20 +386,23 @@ def assiduites_group(with_query: bool = False):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne toutes les assiduités du formsemestre""" """Retourne toutes les assiduités du formsemestre"""
# Récupération du formsemestre à partir du formsemestre_id
formsemestre: FormSemestre = None formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None: if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") 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( assiduites_query = scass.filter_by_formsemestre(
Assiduite.query, Assiduite, formsemestre Assiduite.query, Assiduite, formsemestre
) )
# Filtrage en fonction des paramètres de la requête
if with_query: if with_query:
assiduites_query = _filter_manager(request, assiduites_query) assiduites_query = _filter_manager(request, assiduites_query)
# Préparation du retour JSON
data_set: list[dict] = [] data_set: list[dict] = []
for ass in assiduites_query.all(): for ass in assiduites_query.all():
data = ass.to_dict(format_api=True) data = ass.to_dict(format_api=True)
@ -422,21 +436,28 @@ def count_assiduites_formsemestre(
formsemestre_id: int = None, with_query: bool = False formsemestre_id: int = None, with_query: bool = False
): ):
"""Comptage des assiduités du formsemestre""" """Comptage des assiduités du formsemestre"""
# Récupération du formsemestre à partir du formsemestre_id
formsemestre: FormSemestre = None formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None: if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
# Récupération des étudiants du formsemestre
etuds = formsemestre.etuds.all() etuds = formsemestre.etuds.all()
etuds_id = [etud.id for etud in etuds] 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)) 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 = scass.filter_by_formsemestre(
assiduites_query, Assiduite, formsemestre assiduites_query, Assiduite, formsemestre
) )
# Gestion de la métrique de comptage (all,demi,heure,journee)
metric: str = "all" metric: str = "all"
# Gestion du filtre (en fonction des paramètres de la requête)
filtered: dict = {} filtered: dict = {}
if with_query: if with_query:
metric, filtered = _count_manager(request) 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) etud: Identite = tools.get_etud(etudid, nip, ine)
if etud is None: if etud is None:
return json_error( return json_error(
404, 404,
message="étudiant inconnu", 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: if g.scodoc_dept is None and etud.dept_id is not None:
# route sans département # route sans département
set_sco_dept(etud.departement.acronym) 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) create_list: list[object] = request.get_json(force=True)
# Vérification que c'est bien une liste
if not isinstance(create_list, list): if not isinstance(create_list, list):
return json_error(404, "Le contenu envoyé n'est pas une liste") return json_error(404, "Le contenu envoyé n'est pas une liste")
errors: list = [] # Préparation du retour
success: list = []
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): 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) code, obj = _create_one(data, etud)
if code == 404: if code == 404:
errors.append({"indice": i, "message": obj}) errors.append({"indice": i, "message": obj})
@ -570,53 +604,77 @@ def _create_one(
data: dict, data: dict,
etud: Identite, etud: Identite,
) -> tuple[int, object]: ) -> 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] = [] errors: list[str] = []
# -- vérifications de l'objet json -- # -- vérifications de l'objet json --
# cas 1 : ETAT # cas 1 : ETAT
etat = data.get("etat", None) etat: str = data.get("etat", None)
if etat is None: if etat is None:
errors.append("param 'etat': manquant") errors.append("param 'etat': manquant")
elif not scu.EtatAssiduite.contains(etat): elif not scu.EtatAssiduite.contains(etat):
errors.append("param 'etat': invalide") errors.append("param 'etat': invalide")
etat = scu.EtatAssiduite.get(etat) etat: scu.EtatAssiduite = scu.EtatAssiduite.get(etat)
# cas 2 : date_debut # cas 2 : date_debut
date_debut = data.get("date_debut", None) date_debut: str = data.get("date_debut", None)
if date_debut is None: if date_debut is None:
errors.append("param 'date_debut': manquant") 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: if deb is None:
errors.append("param 'date_debut': format invalide") errors.append("param 'date_debut': format invalide")
# Si datetime sans timezone
elif deb.tzinfo is None: 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 # cas 3 : date_fin (Même fonctionnement ^ )
date_fin = data.get("date_fin", None) date_fin: str = data.get("date_fin", None)
if date_fin is None: if date_fin is None:
errors.append("param 'date_fin': manquant") 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: if fin is None:
errors.append("param 'date_fin': format invalide") errors.append("param 'date_fin': format invalide")
elif fin.tzinfo is None: 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) 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 external_data is not None:
if not isinstance(external_data, dict): if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON") 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_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None moduleimpl: ModuleImpl = None
# On vérifie si le moduleimpl existe (uniquement s'il a été renseigné)
if moduleimpl_id not in [False, None, "", "-1"]: 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": if moduleimpl_id != "autre":
try: try:
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
@ -625,16 +683,23 @@ def _create_one(
if moduleimpl is None: if moduleimpl is None:
errors.append("param 'moduleimpl_id': invalide") errors.append("param 'moduleimpl_id': invalide")
else: else:
# Sinon on met à none le moduleimpl
# et on ajoute dans external data
# le module "autre"
moduleimpl_id = None 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" 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: if errors:
# Construit une chaine de caractère avec les erreurs séparées par `,`
err: str = ", ".join(errors) err: str = ", ".join(errors)
# 404 représente le code d'erreur et err la chaine nouvellement créée
return (404, err) return (404, err)
# TOUT EST OK # SI TOUT EST OK
try: try:
# On essaye de créer l'assiduité
nouv_assiduite: Assiduite = Assiduite.create_assiduite( nouv_assiduite: Assiduite = Assiduite.create_assiduite(
date_debut=deb, date_debut=deb,
date_fin=fin, date_fin=fin,
@ -647,12 +712,23 @@ def _create_one(
notify_mail=True, 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.add(nouv_assiduite)
db.session.commit() db.session.commit()
return (200, {"assiduite_id": nouv_assiduite.id}) return (200, {"assiduite_id": nouv_assiduite.id})
except ScoValueError as excp: except ScoValueError as excp:
# ici on utilise pas json_error car on doit renvoyer status, message # 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] 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) assiduites_list: list[int] = request.get_json(force=True)
if not isinstance(assiduites_list, list): if not isinstance(assiduites_list, list):
return json_error(404, "Le contenu envoyé n'est pas une liste") return json_error(404, "Le contenu envoyé n'est pas une liste")
# Préparation du retour json
output = {"errors": [], "success": []} output = {"errors": [], "success": []}
for i, ass in enumerate(assiduites_list): # Pour chaque assiduite_id on essaye de supprimer l'assiduité
code, msg = _delete_singular(ass, db) 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: if code == 404:
output["errors"].append({"indice": i, "message": msg}) output["errors"].append({"indice": i, "message": msg})
else: else:
@ -692,24 +774,43 @@ def assiduite_delete():
return output return output
def _delete_singular(assiduite_id: int, database) -> tuple[int, str]: def _delete_one(assiduite_id: int) -> tuple[int, str]:
"""@iziram PLEASE COMMENT THIS F*CKING CODE""" """
_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() assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
if assiduite_unique is None: 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" 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: if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None:
# route sans département # route sans département
set_sco_dept(assiduite_unique.etudiant.departement.acronym) 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}") log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}")
Scolog.logdb( Scolog.logdb(
method="delete_assiduite", method="delete_assiduite",
etudid=assiduite_unique.etudiant.id, etudid=assiduite_unique.etudiant.id,
msg=f"assiduité: {assiduite_unique}", msg=f"assiduité: {assiduite_unique}",
) )
database.session.delete(assiduite_unique) db.session.delete(assiduite_unique)
scass.simple_invalidate_cache(ass_dict) # Invalidation du cache
scass.simple_invalidate_cache(assi_dict)
return 200, "OK" return 200, "OK"
@ -730,17 +831,25 @@ def assiduite_edit(assiduite_id: int):
"est_just"?: bool "est_just"?: bool
} }
""" """
# Récupération de l'assiduité à modifier
assiduite_unique: Assiduite = Assiduite.query.filter_by( assiduite_unique: Assiduite = Assiduite.query.filter_by(
id=assiduite_id id=assiduite_id
).first_or_404() ).first_or_404()
errors: list[str] = [] # Récupération des valeurs à modifier
data = request.get_json(force=True) 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: if code == 404:
return json_error(404, obj) return json_error(404, obj)
# Mise à jour de l'assiduité et LOG
log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}") log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}")
Scolog.logdb( Scolog.logdb(
"assiduite_edit", "assiduite_edit",
@ -791,7 +900,7 @@ def assiduites_edit():
) )
continue continue
code, obj = _edit_singular(assi, data) code, obj = _edit_one(assi, data)
obj_retour = { obj_retour = {
"indice": i, "indice": i,
"message": obj, "message": obj,
@ -806,46 +915,69 @@ def assiduites_edit():
return {"errors": errors, "success": success} 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: if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None:
# route sans département # route sans département
set_sco_dept(assiduite_unique.etudiant.departement.acronym) set_sco_dept(assiduite_unique.etudiant.departement.acronym)
errors: list[str] = [] errors: list[str] = []
# Vérifications de data # Vérifications de data
# Cas 1 : Etat # Cas 1 : Etat
if data.get("etat") is not None: 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: if etat is None:
errors.append("param 'etat': invalide") errors.append("param 'etat': invalide")
else: else:
# Mise à jour de l'état
assiduite_unique.etat = etat 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 external_data is not None:
if not isinstance(external_data, dict): if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON") errors.append("param 'external_data' : n'est pas un objet JSON")
else: else:
# Mise à jour de l'external data
assiduite_unique.external_data = external_data assiduite_unique.external_data = external_data
# Cas 2 : Moduleimpl_id # Cas 3 : Moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None moduleimpl: ModuleImpl = None
# False si on modifie pas le moduleimpl
if moduleimpl_id is not False: if moduleimpl_id is not False:
# Si le module n'est pas nul
if moduleimpl_id not in [None, "", "-1"]: if moduleimpl_id not in [None, "", "-1"]:
# Gestion du module Autre
if moduleimpl_id == "autre": if moduleimpl_id == "autre":
# module autre = moduleimpl_id:None + external_data["module"]:"Autre"
assiduite_unique.moduleimpl_id = None assiduite_unique.moduleimpl_id = None
external_data = ( external_data: dict = (
external_data external_data
if external_data is not None and isinstance(external_data, dict) if external_data is not None and isinstance(external_data, dict)
else assiduite_unique.external_data 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" external_data["module"] = "Autre"
assiduite_unique.external_data = external_data assiduite_unique.external_data = external_data
else: else:
# Vérification de l'id et récupération de l'objet ModuleImpl
try: try:
moduleimpl = ModuleImpl.query.filter_by( moduleimpl = ModuleImpl.query.filter_by(
id=int(moduleimpl_id) id=int(moduleimpl_id)
@ -861,8 +993,11 @@ def _edit_singular(assiduite_unique, data):
): ):
errors.append("param 'moduleimpl_id': etud non inscrit") errors.append("param 'moduleimpl_id': etud non inscrit")
else: else:
# Mise à jour du moduleimpl
assiduite_unique.moduleimpl_id = moduleimpl_id assiduite_unique.moduleimpl_id = moduleimpl_id
else: 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( formsemestre: FormSemestre = get_formsemestre_from_data(
assiduite_unique.to_dict() assiduite_unique.to_dict()
) )
@ -873,17 +1008,23 @@ def _edit_singular(assiduite_unique, data):
else: else:
force = scu.is_assiduites_module_forced(dept_id=etud.dept_id) 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( errors.append(
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul" "param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
) )
# Cas 3 : desc # Cas 4 : desc
desc = data.get("desc", False) desc: str = data.get("desc", False)
if desc is not False: if desc is not False:
assiduite_unique.description = desc assiduite_unique.description = desc
# Cas 4 : est_just # Cas 5 : est_just
if assiduite_unique.etat == scu.EtatAssiduite.PRESENT: if assiduite_unique.etat == scu.EtatAssiduite.PRESENT:
assiduite_unique.est_just = False assiduite_unique.est_just = False
else: else:
@ -900,9 +1041,11 @@ def _edit_singular(assiduite_unique, data):
) )
if errors: if errors:
# Retour des erreurs en une seule chaîne séparée par des `,`
err: str = ", ".join(errors) err: str = ", ".join(errors)
return (404, err) return (404, err)
# Mise à jour de l'assiduité et LOG
log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}") log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}")
Scolog.logdb( Scolog.logdb(
"assiduite_edit", "assiduite_edit",
@ -997,21 +1140,34 @@ def _count_manager(requested) -> tuple[str, dict]:
def _filter_manager(requested, assiduites_query: Query) -> Query: 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 # cas 1 : etat assiduite
etat = requested.args.get("etat") etat: str = requested.args.get("etat")
if etat is not None: if etat is not None:
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat) assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
# cas 2 : date de début # 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) deb: datetime = scu.is_iso_formated(
deb, True
) # transformation de la chaine en datetime
# cas 3 : date de fin # cas 3 : date de fin
fin = requested.args.get("date_fin", "").replace(" ", "+") fin: str = requested.args.get("date_fin", "").replace(" ", "+")
fin = scu.is_iso_formated(fin, True) 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): if (deb, fin) != (None, None):
assiduites_query: Query = scass.filter_by_date( assiduites_query: Query = scass.filter_by_date(
assiduites_query, Assiduite, deb, fin assiduites_query, Assiduite, deb, fin
@ -1065,10 +1221,12 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
if user_id is not False: if user_id is not False:
assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id) 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) order = requested.args.get("order", None)
if order is not None: if order is not None:
assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc()) 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) courant = requested.args.get("courant", None)
if courant is not None: if courant is not None:
annee: int = scu.annee_scolaire() annee: int = scu.annee_scolaire()
@ -1081,7 +1239,17 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
return assiduites_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: if request.args.get("with_justifs") is None:
return assi return assi
assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True) assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True)

View File

@ -3,7 +3,7 @@
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE # See LICENSE
############################################################################## ##############################################################################
"""ScoDoc 9 API : Assiduités """ScoDoc 9 API : Justificatifs
""" """
from datetime import datetime from datetime import datetime
@ -28,6 +28,7 @@ from app.models import (
) )
from app.models.assiduites import ( from app.models.assiduites import (
compute_assiduites_justified, compute_assiduites_justified,
get_formsemestre_from_data,
) )
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError 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] query?user_id=[int]
ex query?user_id=3 ex query?user_id=3
""" """
# Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine) etud: Identite = tools.get_etud(etudid, nip, ine)
if etud is None: if etud is None:
@ -122,11 +123,15 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
404, 404,
message="étudiant inconnu", message="étudiant inconnu",
) )
# Récupération des justificatifs de l'étudiant
justificatifs_query = etud.justificatifs justificatifs_query = etud.justificatifs
# Filtrage des justificatifs en fonction de la requête
if with_query: if with_query:
justificatifs_query = _filter_manager(request, justificatifs_query) justificatifs_query = _filter_manager(request, justificatifs_query)
# Mise en forme des données puis retour en JSON
data_set: list[dict] = [] data_set: list[dict] = []
for just in justificatifs_query.all(): for just in justificatifs_query.all():
data = just.to_dict(format_api=True) 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) @permission_required(Permission.ScoView)
def justificatifs_dept(dept_id: int = None, with_query: bool = False): 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: 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] = [] data_set: list[dict] = []
for just in justificatifs_query: for just in justificatifs_query:
data_set.append(_set_sems_and_groupe(just)) data_set.append(_set_sems(just))
return data_set return data_set
def _set_sems_and_groupe(justi: Justificatif) -> dict: def _set_sems(justi: Justificatif) -> dict:
from app.scodoc.sco_groups import get_etud_groups """
_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) data = justi.to_dict(format_api=True)
formsemestre: FormSemestre = ( # Récupération du formsemestre de l'assiduité
FormSemestre.query.join( formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
FormSemestreInscription, # Si le formsemestre existe on l'ajoute au dictionnaire
FormSemestre.id == FormSemestreInscription.formsemestre_id,
)
.filter(
justi.date_debut <= FormSemestre.date_fin,
justi.date_fin >= FormSemestre.date_debut,
FormSemestreInscription.etudid == justi.etudid,
)
.first()
)
if formsemestre: if formsemestre:
data["formsemestre"] = { data["formsemestre"] = {
"id": formsemestre.id, "id": formsemestre.id,
"title": formsemestre.session_id(), "title": formsemestre.session_id(),
} }
return data return data
@ -208,20 +220,27 @@ def _set_sems_and_groupe(justi: Justificatif) -> dict:
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne tous les justificatifs du formsemestre""" """Retourne tous les justificatifs du formsemestre"""
# Récupération du formsemestre
formsemestre: FormSemestre = None formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id) 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: if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas") 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 Justificatif.query, Justificatif, formsemestre
) )
# Filtrage des justificatifs
if with_query: 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] = [] data_set: list[dict] = []
for justi in justificatifs_query.all(): for justi in justificatifs_query.all():
data = justi.to_dict(format_api=True) 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) etud: Identite = tools.get_etud(etudid, nip, ine)
if etud is None: if etud is None:
@ -272,16 +293,22 @@ def justif_create(etudid: int = None, nip=None, ine=None):
message="étudiant inconnu", message="étudiant inconnu",
) )
# Récupération des justificatifs à créer
create_list: list[object] = request.get_json(force=True) create_list: list[object] = request.get_json(force=True)
if not isinstance(create_list, list): if not isinstance(create_list, list):
return json_error(404, "Le contenu envoyé n'est pas une liste") return json_error(404, "Le contenu envoyé n'est pas une liste")
errors: list = [] errors: list[dict] = []
success: list = [] success: list[dict] = []
justifs: list = [] justifs: list[Justificatif] = []
# énumération des justificatifs
for i, data in enumerate(create_list): for i, data in enumerate(create_list):
code, obj, justi = _create_one(data, etud) code, obj, justi = _create_one(data, etud)
code: int
obj: str | dict
justi: Justificatif | None
if code == 404: if code == 404:
errors.append({"indice": i, "message": obj}) errors.append({"indice": i, "message": obj})
else: else:
@ -289,6 +316,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
justifs.append(justi) justifs.append(justi)
scass.simple_invalidate_cache(data, etud.id) 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) compute_assiduites_justified(etud.etudid, justifs)
return {"errors": errors, "success": success} return {"errors": errors, "success": success}
@ -296,32 +324,32 @@ def justif_create(etudid: int = None, nip=None, ine=None):
def _create_one( def _create_one(
data: dict, data: dict,
etud: Identite, etud: Identite,
) -> tuple[int, object]: ) -> tuple[int, object, Justificatif]:
errors: list[str] = [] errors: list[str] = []
# -- vérifications de l'objet json -- # -- vérifications de l'objet json --
# cas 1 : ETAT # cas 1 : ETAT
etat = data.get("etat", None) etat: str = data.get("etat", None)
if etat is None: if etat is None:
errors.append("param 'etat': manquant") errors.append("param 'etat': manquant")
elif not scu.EtatJustificatif.contains(etat): elif not scu.EtatJustificatif.contains(etat):
errors.append("param 'etat': invalide") errors.append("param 'etat': invalide")
etat = scu.EtatJustificatif.get(etat) etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat)
# cas 2 : date_debut # cas 2 : date_debut
date_debut = data.get("date_debut", None) date_debut: str = data.get("date_debut", None)
if date_debut is None: if date_debut is None:
errors.append("param 'date_debut': manquant") 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: if deb is None:
errors.append("param 'date_debut': format invalide") errors.append("param 'date_debut': format invalide")
# cas 3 : date_fin # cas 3 : date_fin
date_fin = data.get("date_fin", None) date_fin: str = data.get("date_fin", None)
if date_fin is None: if date_fin is None:
errors.append("param 'date_fin': manquant") 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: if fin is None:
errors.append("param 'date_fin': format invalide") errors.append("param 'date_fin': format invalide")
@ -329,7 +357,7 @@ def _create_one(
raison: str = data.get("raison", None) 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 external_data is not None:
if not isinstance(external_data, dict): if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON") errors.append("param 'external_data' : n'est pas un objet JSON")
@ -341,6 +369,7 @@ def _create_one(
# TOUT EST OK # TOUT EST OK
try: try:
# On essaye de créer le justificatif
nouv_justificatif: Query = Justificatif.create_justificatif( nouv_justificatif: Query = Justificatif.create_justificatif(
date_debut=deb, date_debut=deb,
date_fin=fin, date_fin=fin,
@ -351,6 +380,11 @@ def _create_one(
external_data=external_data, 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.add(nouv_justificatif)
db.session.commit() db.session.commit()
@ -363,10 +397,7 @@ def _create_one(
nouv_justificatif, nouv_justificatif,
) )
except ScoValueError as excp: except ScoValueError as excp:
return ( return (404, excp.args[0], None)
404,
excp.args[0],
)
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"]) @bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
@ -387,53 +418,58 @@ def justif_edit(justif_id: int):
"date_fin"?: str "date_fin"?: str
} }
""" """
# Récupération du justificatif à modifier
justificatif_unique: Query = Justificatif.query.filter_by( justificatif_unique: Query = Justificatif.query.filter_by(
id=justif_id id=justif_id
).first_or_404() ).first_or_404()
errors: list[str] = [] errors: list[str] = []
data = request.get_json(force=True) 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) avant_ids: list[int] = scass.justifies(justificatif_unique)
# Vérifications de data # Vérifications de data
# Cas 1 : Etat # Cas 1 : Etat
if data.get("etat") is not None: 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: if etat is None:
errors.append("param 'etat': invalide") errors.append("param 'etat': invalide")
else: else:
justificatif_unique.etat = etat justificatif_unique.etat = etat
# Cas 2 : raison # Cas 2 : raison
raison = data.get("raison", False) raison: str = data.get("raison", False)
if raison is not False: if raison is not False:
justificatif_unique.raison = raison justificatif_unique.raison = raison
deb, fin = None, None deb, fin = None, None
# cas 3 : date_debut # 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 not False:
if date_debut is None: if date_debut is None:
errors.append("param 'date_debut': manquant") 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: if deb is None:
errors.append("param 'date_debut': format invalide") errors.append("param 'date_debut': format invalide")
# cas 4 : date_fin # 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 not False:
if date_fin is None: if date_fin is None:
errors.append("param 'date_fin': manquant") 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: if fin is None:
errors.append("param 'date_fin': format invalide") 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 deb = deb if deb is not None else justificatif_unique.date_debut
fin = fin if fin is not None else justificatif_unique.date_fin 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 external_data is not None:
if not isinstance(external_data, dict): if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON") errors.append("param 'external_data' : n'est pas un objet JSON")
@ -443,6 +479,7 @@ def justif_edit(justif_id: int):
if fin <= deb: if fin <= deb:
errors.append("param 'dates' : Date de début après date de fin") 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_debut = deb
justificatif_unique.date_fin = fin justificatif_unique.date_fin = fin
@ -450,9 +487,14 @@ def justif_edit(justif_id: int):
err: str = ", ".join(errors) err: str = ", ".join(errors)
return json_error(404, err) return json_error(404, err)
# Mise à jour du justificatif
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
db.session.commit() 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 = { retour = {
"couverture": { "couverture": {
"avant": avant_ids, "avant": avant_ids,
@ -463,7 +505,7 @@ def justif_edit(justif_id: int):
), ),
} }
} }
# Invalide le cache
scass.simple_invalidate_cache(justificatif_unique.to_dict()) scass.simple_invalidate_cache(justificatif_unique.to_dict())
return retour return retour
@ -487,6 +529,8 @@ def justif_delete():
""" """
# Récupération des justif_ids
justificatifs_list: list[int] = request.get_json(force=True) justificatifs_list: list[int] = request.get_json(force=True)
if not isinstance(justificatifs_list, list): if not isinstance(justificatifs_list, list):
return json_error(404, "Le contenu envoyé n'est pas une liste") return json_error(404, "Le contenu envoyé n'est pas une liste")
@ -494,7 +538,7 @@ def justif_delete():
output = {"errors": [], "success": []} output = {"errors": [], "success": []}
for i, ass in enumerate(justificatifs_list): for i, ass in enumerate(justificatifs_list):
code, msg = _delete_singular(ass, db) code, msg = _delete_one(ass)
if code == 404: if code == 404:
output["errors"].append({"indice": i, "message": msg}) output["errors"].append({"indice": i, "message": msg})
else: else:
@ -505,22 +549,41 @@ def justif_delete():
return output return output
def _delete_singular(justif_id: int, database): def _delete_one(justif_id: int) -> tuple[int, str]:
justificatif_unique: Query = Justificatif.query.filter_by(id=justif_id).first() """
_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: if justificatif_unique is None:
return (404, "Justificatif non existant") return (404, "Justificatif non existant")
# Récupération de l'archive du justificatif
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
if archive_name is not None: if archive_name is not None:
# Si elle existe : on essaye de la supprimer
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
try: try:
archiver.delete_justificatif(justificatif_unique.etudiant, archive_name) archiver.delete_justificatif(justificatif_unique.etudiant, archive_name)
except ValueError: except ValueError:
pass pass
# On invalide le cache
scass.simple_invalidate_cache(justificatif_unique.to_dict()) 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( compute_assiduites_justified(
justificatif_unique.etudid, justificatif_unique.etudid,
Justificatif.query.filter_by(etudid=justificatif_unique.etudid).all(), 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) Importation d'un fichier (création d'archive)
""" """
# On vérifie qu'un fichier a bien été envoyé
if len(request.files) == 0: if len(request.files) == 0:
return json_error(404, "Il n'y a pas de fichier joint") return json_error(404, "Il n'y a pas de fichier joint")
file = list(request.files.values())[0] file = list(request.files.values())[0]
if file.filename == "": if file.filename == "":
return json_error(404, "Il n'y a pas de fichier joint") 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) query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404() justificatif_unique: Justificatif = query.first_or_404()
# Récupération de l'archive si elle existe
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
# Utilisation de l'archiver de justificatifs
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
try: try:
# On essaye de sauvegarder le fichier
fname: str fname: str
archive_name, fname = archiver.save_justificatif( archive_name, fname = archiver.save_justificatif(
justificatif_unique.etudiant, justificatif_unique.etudiant,
@ -567,6 +634,7 @@ def justif_import(justif_id: int = None):
user_id=current_user.id, user_id=current_user.id,
) )
# On actualise l'archive du justificatif
justificatif_unique.fichier = archive_name justificatif_unique.fichier = archive_name
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
@ -574,6 +642,7 @@ def justif_import(justif_id: int = None):
return {"filename": fname} return {"filename": fname}
except ScoValueError as err: except ScoValueError as err:
# Si cela ne fonctionne pas on renvoie une erreur
return json_error(404, err.args[0]) 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 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) query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404() justificatif_unique: Justificatif = query.first_or_404()
# On récupère l'archive concernée
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
if archive_name is None: 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") 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() archiver: JustificatifArchiver = JustificatifArchiver()
try: try:
return archiver.get_justificatif_file( return archiver.get_justificatif_file(
archive_name, justificatif_unique.etudiant, filename archive_name, justificatif_unique.etudiant, filename
) )
except ScoValueError as err: except ScoValueError as err:
# On retourne une erreur json si jamais il y a un problème
return json_error(404, err.args[0]) 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): def justif_remove(justif_id: int = None):
""" """
Supression d'un fichier ou d'une archive Supression d'un fichier ou d'une archive
# TOTALK: Doc, expliquer les noms coté server
{ {
"remove": <"all"/"list"> "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) data: dict = request.get_json(force=True)
# On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404() justificatif_unique: Justificatif = query.first_or_404()
# On récupère l'archive
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
if archive_name is None: 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") 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") remove: str = data.get("remove")
if remove is None or remove not in ("all", "list"): if remove is None or remove not in ("all", "list"):
return json_error(404, "param 'remove': Valeur invalide") return json_error(404, "param 'remove': Valeur invalide")
# On récupère l'archiver et l'étudiant
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
etud = justificatif_unique.etudiant etud = justificatif_unique.etudiant
try: try:
if remove == "all": if remove == "all":
# Suppression de toute l'archive du justificatif
archiver.delete_justificatif(etud, archive_name=archive_name) archiver.delete_justificatif(etud, archive_name=archive_name)
justificatif_unique.fichier = None justificatif_unique.fichier = None
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
db.session.commit() db.session.commit()
else: else:
# Suppression des fichiers dont le nom se trouve dans la liste "filenames"
for fname in data.get("filenames", []): for fname in data.get("filenames", []):
archiver.delete_justificatif( archiver.delete_justificatif(
etud, etud,
@ -659,6 +740,7 @@ def justif_remove(justif_id: int = None):
filename=fname, 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: if len(archiver.list_justificatifs(archive_name, etud)) == 0:
archiver.delete_justificatif(etud, archive_name) archiver.delete_justificatif(etud, archive_name)
justificatif_unique.fichier = None justificatif_unique.fichier = None
@ -666,8 +748,10 @@ def justif_remove(justif_id: int = None):
db.session.commit() db.session.commit()
except ScoValueError as err: 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]) return json_error(404, err.args[0])
# On retourne une réponse "removed" si tout s'est bien passé
return {"response": "removed"} return {"response": "removed"}
@ -682,29 +766,36 @@ def justif_list(justif_id: int = None):
Liste les fichiers du justificatif Liste les fichiers du justificatif
""" """
# Récupération du justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404() justificatif_unique: Justificatif = query.first_or_404()
# Récupération de l'archive avec l'archiver
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
filenames: list[str] = [] filenames: list[str] = []
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None: if archive_name is not None:
filenames = archiver.list_justificatifs( filenames = archiver.list_justificatifs(
archive_name, justificatif_unique.etudiant 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": []} 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: for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission( if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.AbsJustifView Permission.AbsJustifView
): ):
retour["filenames"].append(filename[0]) retour["filenames"].append(filename[0])
# On renvoie le total et la liste des fichiers visibles
return retour return retour
@ -720,44 +811,45 @@ def justif_justifies(justif_id: int = None):
Liste assiduite_id justifiées par le justificatif Liste assiduite_id justifiées par le justificatif
""" """
# On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) query: Query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404() 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) assiduites_list: list[int] = scass.justifies(justificatif_unique)
# On la renvoie
return assiduites_list return assiduites_list
# -- Utils -- # -- 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 Retourne les justificatifs entrés filtrés en fonction de la request
""" """
# cas 1 : etat justificatif # cas 1 : etat justificatif
etat = requested.args.get("etat") etat: str = requested.args.get("etat")
if etat is not None: if etat is not None:
justificatifs_query = scass.filter_justificatifs_by_etat( justificatifs_query: Query = scass.filter_justificatifs_by_etat(
justificatifs_query, etat justificatifs_query, etat
) )
# cas 2 : date de début # 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) deb: datetime = scu.is_iso_formated(deb, True)
# cas 3 : date de fin # cas 3 : date de fin
fin = requested.args.get("date_fin", "").replace(" ", "+") fin: str = requested.args.get("date_fin", "").replace(" ", "+")
fin = scu.is_iso_formated(fin, True) fin: datetime = scu.is_iso_formated(fin, True)
if (deb, fin) != (None, None): if (deb, fin) != (None, None):
justificatifs_query: Query = scass.filter_by_date( justificatifs_query: Query = scass.filter_by_date(
justificatifs_query, Justificatif, deb, fin justificatifs_query, Justificatif, deb, fin
) )
# cas 4 : user_id
user_id = requested.args.get("user_id", False) user_id = requested.args.get("user_id", False)
if user_id is not False: if user_id is not False:
justificatifs_query: Query = scass.filter_by_user_id( justificatifs_query: Query = scass.filter_by_user_id(
@ -778,12 +870,13 @@ def _filter_manager(requested, justificatifs_query):
except ValueError: except ValueError:
formsemestre = None formsemestre = None
# cas 6 : order (retourne les justificatifs par ordre décroissant de date_debut)
order = requested.args.get("order", None) order = requested.args.get("order", None)
if order is not None: if order is not None:
justificatifs_query: Query = justificatifs_query.order_by( justificatifs_query: Query = justificatifs_query.order_by(
Justificatif.date_debut.desc() Justificatif.date_debut.desc()
) )
# cas 7 : courant (retourne uniquement les justificatifs de l'année scolaire courante)
courant = requested.args.get("courant", None) courant = requested.args.get("courant", None)
if courant is not None: if courant is not None:
annee: int = scu.annee_scolaire() annee: int = scu.annee_scolaire()
@ -793,6 +886,7 @@ def _filter_manager(requested, justificatifs_query):
Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee), 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) group_id = requested.args.get("group_id", None)
if group_id is not None: if group_id is not None:
try: try:

View File

@ -37,7 +37,9 @@ import datetime
class TimeField(StringField): class TimeField(StringField):
"""HTML5 time input.""" """HTML5 time input.
tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f
"""
widget = TimeInput() widget = TimeInput()

View File

@ -82,6 +82,7 @@ class Assiduite(db.Model):
etat = self.etat etat = self.etat
user: User = None user: User = None
if format_api: if format_api:
# format api utilise les noms "present,absent,retard" au lieu des int
etat = EtatAssiduite.inverse().get(self.etat).name etat = EtatAssiduite.inverse().get(self.etat).name
if self.user_id is not None: if self.user_id is not None:
user = db.session.get(User, self.user_id) 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 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_debut = localize_datetime(date_debut)
date_fin = localize_datetime(date_fin) 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( count: int = collection.filter(
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
).count() ).count()
@ -375,19 +371,26 @@ def compute_assiduites_justified(
Returns: Returns:
list[int]: la liste des assiduités qui ont été justifiées. 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: 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] 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: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites_justifiees: list[int] = [] assiduites_justifiees: list[int] = []
for assi in assiduites: for assi in assiduites:
# On ne justifie pas les Présences
if assi.etat == EtatAssiduite.PRESENT: if assi.etat == EtatAssiduite.PRESENT:
continue continue
# On récupère les justificatifs qui justifient l'assiduité `assi`
assi_justificatifs = Justificatif.query.filter( assi_justificatifs = Justificatif.query.filter(
Justificatif.etudid == assi.etudid, Justificatif.etudid == assi.etudid,
Justificatif.date_debut <= assi.date_debut, Justificatif.date_debut <= assi.date_debut,
@ -395,21 +398,39 @@ def compute_assiduites_justified(
Justificatif.etat == EtatJustificatif.VALIDE, Justificatif.etat == EtatJustificatif.VALIDE,
).all() ).all()
# Si au moins un justificatif possède une période qui couvre l'assiduité
if any( if any(
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
for j in justificatifs + assi_justificatifs 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 assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id) assiduites_justifiees.append(assi.assiduite_id)
db.session.add(assi) db.session.add(assi)
elif reset: 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 assi.est_just = False
db.session.add(assi) db.session.add(assi)
# On valide la session
db.session.commit() db.session.commit()
# On renvoie la liste des assiduite_id des assiduités justifiées
return assiduites_justifiees 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) assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long) 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, date_fin: datetime,
long: bool = False, long: bool = False,
valid: 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( justifs: Query = Justificatif.query.filter(
Justificatif.etudid == etudid, Justificatif.etudid == etudid,
Justificatif.date_debut <= date_debut, Justificatif.date_debut <= date_debut,
Justificatif.date_fin >= date_fin, Justificatif.date_fin >= date_fin,
) )
# si valide est vrai alors on filtre pour n'avoir que les justificatifs valide
if valid: if valid:
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE) 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] 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: 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 ( return (
FormSemestre.query.join( FormSemestre.query.join(
FormSemestreInscription, FormSemestreInscription,

View File

@ -755,9 +755,7 @@ function isConflictSameAsPeriod(conflict, period = undefined) {
* @returns {Date} la date sélectionnée * @returns {Date} la date sélectionnée
*/ */
function getDate() { function getDate() {
const date = new Date( const date = new Date(document.querySelector("#tl_date").value);
document.querySelector("#tl_date").getAttribute("value")
);
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
return date; return date;
} }
@ -1652,9 +1650,6 @@ function fastJustify(assiduite) {
fin: new moment.tz(assiduite.date_fin, TIMEZONE), fin: new moment.tz(assiduite.date_fin, TIMEZONE),
}; };
const action = (justifs) => { const action = (justifs) => {
if (justifs.length > 0) {
justifyAssiduite(assiduite.assiduite_id, !assiduite.est_just);
} else {
//créer un nouveau justificatif //créer un nouveau justificatif
// Afficher prompt -> demander raison et état // Afficher prompt -> demander raison et état
@ -1701,7 +1696,6 @@ function fastJustify(assiduite) {
() => {}, () => {},
"#7059FF" "#7059FF"
); );
}
}; };
if (assiduite.etudid) { if (assiduite.etudid) {
getJustificatifFromPeriod(period, assiduite.etudid, action); getJustificatifFromPeriod(period, assiduite.etudid, action);
@ -1804,7 +1798,9 @@ function getModuleImpl(assiduite) {
assiduite.external_data != null && assiduite.external_data != null &&
assiduite.external_data.hasOwnProperty("module") assiduite.external_data.hasOwnProperty("module")
) { ) {
return assiduite.external_data.module; return assiduite.external_data.module == "Autre"
? "Tout module"
: assiduite.external_data.module;
} else { } else {
return "Pas de module"; return "Pas de module";
} }

View File

@ -345,8 +345,8 @@
let assi = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id })[0] let assi = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id })[0]
li.addEventListener('click', () => { li.addEventListener('click', () => {
if (assiduite && !assiduite[0].est_just && assiduite[0].etat != "PRESENT") { if (assi && !assi.est_just && assi.etat != "PRESENT") {
fastJustify(assiduite[0]) fastJustify(assi)
} else { } else {
openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée.")) openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée."))
} }

View File

@ -160,6 +160,8 @@ class HTMLBuilder:
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def bilan_dept(): def bilan_dept():
"""Gestionnaire assiduités, page principale""" """Gestionnaire assiduités, page principale"""
# Préparation de la page
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Saisie de l'assiduité", page_title="Saisie de l'assiduité",
@ -183,7 +185,10 @@ def bilan_dept():
"""<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant, """<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
choisissez d'abord la personne concernée&nbsp;:</p>""" choisissez d'abord la personne concernée&nbsp;:</p>"""
) )
# Ajout de la barre de recherche d'étudiant (redirection vers bilan etud)
H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud")) H.append(sco_find_etud.form_search_etud(dest_url="assiduites.bilan_etud"))
# Gestion des billets d'absences
if current_user.has_permission( if current_user.has_permission(
Permission.AbsChange Permission.AbsChange
) and sco_preferences.get_preference("handle_billets_abs"): ) and sco_preferences.get_preference("handle_billets_abs"):
@ -195,19 +200,23 @@ def bilan_dept():
</li></ul> </li></ul>
""" """
) )
# Récupération des années d'étude du département
# (afin de sélectionner une année)
dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first() dept: Departement = Departement.query.filter_by(id=g.scodoc_dept_id).first()
annees: list[int] = sorted( annees: list[int] = sorted(
[f.date_debut.year for f in dept.formsemestres], [f.date_debut.year for f in dept.formsemestres],
reverse=True, reverse=True,
) )
annee = scu.annee_scolaire() # Année courante, sera utilisée par défaut
annee = scu.annee_scolaire() # Génération d'une liste "json" d'années
annees_str: str = "[" annees_str: str = "["
for ann in annees: for ann in annees:
annees_str += f"{ann}," annees_str += f"{ann},"
annees_str += "]" annees_str += "]"
# Récupération d'un formsemestre
# (pour n'afficher que les assiduites/justificatifs liés au formsemestre)
formsemestre_id = request.args.get("formsemestre_id", "") formsemestre_id = request.args.get("formsemestre_id", "")
if formsemestre_id: if formsemestre_id:
try: try:
@ -216,6 +225,7 @@ def bilan_dept():
except AttributeError: except AttributeError:
formsemestre_id = "" formsemestre_id = ""
# Peuplement du template jinja
H.append( H.append(
render_template( render_template(
"assiduites/pages/bilan_dept.j2", "assiduites/pages/bilan_dept.j2",
@ -230,49 +240,6 @@ def bilan_dept():
return "\n".join(H) return "\n".join(H)
# @bp.route("/ListeSemestre")
# @scodoc
# @permission_required(Permission.ScoView)
# def liste_assiduites_formsemestre():
# """
# liste_assiduites_etud Affichage de toutes les assiduites et justificatifs d'un etudiant
# Args:
# etudid (int): l'identifiant de l'étudiant
# Returns:
# str: l'html généré
# """
# formsemestre_id = request.args.get("formsemestre_id", -1)
# formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
# if formsemestre.dept_id != g.scodoc_dept_id:
# abort(404, "FormSemestre inexistant dans ce département")
# header: str = html_sco_header.sco_header(
# page_title="Liste des assiduités du semestre",
# init_qtip=True,
# javascripts=[
# "js/assiduites.js",
# "libjs/moment-2.29.4.min.js",
# "libjs/moment-timezone.js",
# ],
# cssstyles=CSSSTYLES
# + [
# "css/assiduites.css",
# ],
# )
# return HTMLBuilder(
# header,
# render_template(
# "assiduites/pages/liste_semestre.j2",
# sco=ScoData(formsemestre=formsemestre),
# sem=formsemestre.titre_annee(),
# formsemestre_id=formsemestre.id,
# ),
# ).build()
@bp.route("/SignaleAssiduiteEtud") @bp.route("/SignaleAssiduiteEtud")
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
@ -287,22 +254,22 @@ def signal_assiduites_etud():
str: l'html généré str: l'html généré
""" """
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1) etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Récupération de la date (par défaut la date du jour)
date = request.args.get("date", datetime.date.today().isoformat()) date = request.args.get("date", datetime.date.today().isoformat())
# gestion évaluations # gestion évaluations (Appel à la page depuis les évaluations)
saisie_eval: bool = request.args.get("saisie_eval") is not None saisie_eval: bool = request.args.get("saisie_eval") is not None
date_deb: str = request.args.get("date_deb") date_deb: str = request.args.get("date_deb")
date_fin: str = request.args.get("date_fin") date_fin: str = request.args.get("date_fin")
moduleimpl_id: int = request.args.get("moduleimpl_id", "") moduleimpl_id: int = request.args.get("moduleimpl_id", "")
evaluation_id: int = request.args.get("evaluation_id") evaluation_id: int = request.args.get("evaluation_id")
redirect_url: str = ( redirect_url: str = (
"#" "#"
if not saisie_eval if not saisie_eval
@ -313,6 +280,7 @@ def signal_assiduites_etud():
) )
) )
# Préparation de la page (Header)
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title="Saisie assiduité", page_title="Saisie assiduité",
init_qtip=True, init_qtip=True,
@ -335,11 +303,14 @@ def signal_assiduites_etud():
"assi_afternoon_time", "18:00:00" "assi_afternoon_time", "18:00:00"
) )
# Gestion du selecteur de moduleimpl (pour le tableau différé)
select = f""" select = f"""
<select class="dynaSelect"> <select class="dynaSelect">
{render_template("assiduites/widgets/simplemoduleimpl_select.j2")} {render_template("assiduites/widgets/simplemoduleimpl_select.j2")}
</select> </select>
""" """
# Génération de la page
return HTMLBuilder( return HTMLBuilder(
header, header,
_mini_timeline(), _mini_timeline(),
@ -381,13 +352,16 @@ def liste_assiduites_etud():
str: l'html généré str: l'html généré
""" """
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1) etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Gestion d'une assiduité unique (redirigé depuis le calendrier)
assiduite_id: int = request.args.get("assiduite_id", -1) assiduite_id: int = request.args.get("assiduite_id", -1)
# Préparation de la page
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title=f"Assiduité de {etud.nomprenom}", page_title=f"Assiduité de {etud.nomprenom}",
init_qtip=True, init_qtip=True,
@ -401,7 +375,7 @@ def liste_assiduites_etud():
"css/assiduites.css", "css/assiduites.css",
], ],
) )
# Peuplement du template jinja
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template( render_template(
@ -429,12 +403,13 @@ def bilan_etud():
Returns: Returns:
str: l'html généré str: l'html généré
""" """
# Récupération de l'étudiant
etudid = request.args.get("etudid", -1) etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Préparation de la page (header)
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title=f"Bilan de l'assiduité de {etud.nomprenom}", page_title=f"Bilan de l'assiduité de {etud.nomprenom}",
init_qtip=True, init_qtip=True,
@ -449,13 +424,16 @@ def bilan_etud():
], ],
) )
# Gestion des dates du bilan (par défaut l'année scolaire)
date_debut: str = f"{scu.annee_scolaire()}-09-01" date_debut: str = f"{scu.annee_scolaire()}-09-01"
date_fin: str = f"{scu.annee_scolaire()+1}-06-30" date_fin: str = f"{scu.annee_scolaire()+1}-06-30"
# Récupération de la métrique d'assiduité
assi_metric = scu.translate_assiduites_metric( assi_metric = scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id), sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
) )
# Génération de la page
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template( render_template(
@ -486,11 +464,13 @@ def ajout_justificatif_etud():
str: l'html généré str: l'html généré
""" """
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1) etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Préparation de la page (header)
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title="Justificatifs", page_title="Justificatifs",
init_qtip=True, init_qtip=True,
@ -505,6 +485,7 @@ def ajout_justificatif_etud():
], ],
) )
# Peuplement du template jinja
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template( render_template(
@ -533,11 +514,13 @@ def calendrier_etud():
str: l'html généré str: l'html généré
""" """
# Récupération de l'étudiant
etudid = request.args.get("etudid", -1) etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Préparation de la page
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title="Calendrier de l'assiduité", page_title="Calendrier de l'assiduité",
init_qtip=True, init_qtip=True,
@ -552,16 +535,20 @@ def calendrier_etud():
], ],
) )
# Récupération des années d'étude de l'étudiant
annees: list[int] = sorted( annees: list[int] = sorted(
[ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions], [ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions],
reverse=True, reverse=True,
) )
# Transformation en une liste "json"
# (sera utilisé pour générer le selecteur d'année)
annees_str: str = "[" annees_str: str = "["
for ann in annees: for ann in annees:
annees_str += f"{ann}," annees_str += f"{ann},"
annees_str += "]" annees_str += "]"
# Peuplement du template jinja
return HTMLBuilder( return HTMLBuilder(
header, header,
render_template( render_template(
@ -585,11 +572,11 @@ def signal_assiduites_group():
Returns: Returns:
str: l'html généré str: l'html généré
""" """
# Récupération des paramètres de l'url
formsemestre_id: int = request.args.get("formsemestre_id", -1) formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id") moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat()) date: str = request.args.get("jour", datetime.date.today().isoformat())
group_ids: list[int] = request.args.get("group_ids", None) group_ids: list[int] = request.args.get("group_ids", None)
if group_ids is None: if group_ids is None:
group_ids = [] group_ids = []
else: else:
@ -601,12 +588,14 @@ def signal_assiduites_group():
moduleimpl_id = int(moduleimpl_id) moduleimpl_id = int(moduleimpl_id)
except (TypeError, ValueError): except (TypeError, ValueError):
moduleimpl_id = None moduleimpl_id = None
# Vérification du formsemestre_id # Vérification du formsemestre_id
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
except (TypeError, ValueError): except (TypeError, ValueError):
formsemestre_id = None formsemestre_id = None
# Gestion des groupes
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
) )
@ -617,10 +606,6 @@ def signal_assiduites_group():
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
# --- URL DEFAULT ---
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
# --- Filtrage par formsemestre --- # --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id formsemestre_id = groups_infos.formsemestre_id
@ -628,17 +613,22 @@ def signal_assiduites_group():
if formsemestre.dept_id != g.scodoc_dept_id: if formsemestre.dept_id != g.scodoc_dept_id:
abort(404, "groupes inexistants dans ce département") abort(404, "groupes inexistants dans ce département")
# Vérification du forçage du module
require_module = sco_preferences.get_preference("forcer_module", formsemestre_id) require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
# Récupération des étudiants des groupes
etuds = [ etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members for m in groups_infos.members
] ]
# --- Vérification de la date --- # --- Vérification de la date ---
real_date = scu.is_iso_formated(date, True).date() real_date = scu.is_iso_formated(date, True).date()
if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin: if real_date < formsemestre.date_debut or real_date > formsemestre.date_fin:
# Si le jour est hors semestre, indiquer une erreur
# Formatage des dates pour le message d'erreur
real_str = real_date.strftime("%d/%m/%Y") real_str = real_date.strftime("%d/%m/%Y")
form_deb = formsemestre.date_debut.strftime("%d/%m/%Y") form_deb = formsemestre.date_debut.strftime("%d/%m/%Y")
form_fin = formsemestre.date_fin.strftime("%d/%m/%Y") form_fin = formsemestre.date_fin.strftime("%d/%m/%Y")
@ -662,8 +652,7 @@ def signal_assiduites_group():
# Si aucun etudiant n'est inscrit au module choisi... # Si aucun etudiant n'est inscrit au module choisi...
moduleimpl_id = None moduleimpl_id = None
# --- Génération de l'HTML --- # Récupération du nom des/du groupe(s)
sem = formsemestre.to_dict()
if groups_infos.tous_les_etuds_du_sem: if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en" gr_tit = "en"
@ -676,12 +665,15 @@ def signal_assiduites_group():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>" grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
) )
# --- Génération de l'HTML ---
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title="Saisie journalière des assiduités", page_title="Saisie journalière des assiduités",
init_qtip=True, init_qtip=True,
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [ + [
# Voir fonctionnement JS # Voir fonctionnement JS
# XXX Retirer moment
"js/etud_info.js", "js/etud_info.js",
"js/groups_view.js", "js/groups_view.js",
"js/assiduites.js", "js/assiduites.js",
@ -694,6 +686,10 @@ def signal_assiduites_group():
], ],
) )
# Récupération du semestre en dictionnaire
sem = formsemestre.to_dict()
# Peuplement du template jinja
return HTMLBuilder( return HTMLBuilder(
header, header,
_mini_timeline(), _mini_timeline(),
@ -731,11 +727,12 @@ def visu_assiduites_group():
Returns: Returns:
str: l'html généré str: l'html généré
""" """
# Récupération des paramètres de la requête
formsemestre_id: int = request.args.get("formsemestre_id", -1) formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id") moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat()) date: str = request.args.get("jour", datetime.date.today().isoformat())
group_ids: list[int] = request.args.get("group_ids", None) group_ids: list[int] = request.args.get("group_ids", None)
if group_ids is None: if group_ids is None:
group_ids = [] group_ids = []
else: else:
@ -755,6 +752,7 @@ def visu_assiduites_group():
except (TypeError, ValueError) as exc: except (TypeError, ValueError) as exc:
raise ScoValueError("identifiant de formsemestre invalide") from exc raise ScoValueError("identifiant de formsemestre invalide") from exc
# Récupérations des/du groupe(s)
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
) )
@ -765,10 +763,6 @@ def visu_assiduites_group():
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
# --- URL DEFAULT ---
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
# --- Filtrage par formsemestre --- # --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id formsemestre_id = groups_infos.formsemestre_id
@ -776,10 +770,10 @@ def visu_assiduites_group():
if formsemestre.dept_id != g.scodoc_dept_id: if formsemestre.dept_id != g.scodoc_dept_id:
abort(404, "groupes inexistants dans ce département") abort(404, "groupes inexistants dans ce département")
require_module = sco_preferences.get_preference( # Vérfication du forçage du module
"abs_require_module", formsemestre_id require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
)
# Récupération des étudiants du/des groupe(s)
etuds = [ etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members for m in groups_infos.members
@ -810,7 +804,6 @@ def visu_assiduites_group():
moduleimpl_id = None moduleimpl_id = None
# --- Génération de l'HTML --- # --- Génération de l'HTML ---
sem = formsemestre.to_dict()
if groups_infos.tous_les_etuds_du_sem: if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en" gr_tit = "en"
@ -841,6 +834,9 @@ def visu_assiduites_group():
], ],
) )
# Récupération du semestre en dictionnaire
sem = formsemestre.to_dict()
return HTMLBuilder( return HTMLBuilder(
header, header,
_mini_timeline(), _mini_timeline(),
@ -873,10 +869,14 @@ def visu_assiduites_group():
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def etat_abs_date(): def etat_abs_date():
"""date_debut, date_fin en ISO""" """date_debut, date_fin en ISO"""
# Récupération des paramètre de la requête
date_debut_str = request.args.get("date_debut") date_debut_str = request.args.get("date_debut")
date_fin_str = request.args.get("date_fin") date_fin_str = request.args.get("date_fin")
title = request.args.get("desc") title = request.args.get("desc")
group_ids: list[int] = request.args.get("group_ids", None) group_ids: list[int] = request.args.get("group_ids", None)
# Vérification des dates
try: try:
date_debut = datetime.datetime.fromisoformat(date_debut_str) date_debut = datetime.datetime.fromisoformat(date_debut_str)
except ValueError as exc: except ValueError as exc:
@ -885,6 +885,8 @@ def etat_abs_date():
date_fin = datetime.datetime.fromisoformat(date_fin_str) date_fin = datetime.datetime.fromisoformat(date_fin_str)
except ValueError as exc: except ValueError as exc:
raise ScoValueError("date_fin invalide") from exc raise ScoValueError("date_fin invalide") from exc
# Vérification des groupes
if group_ids is None: if group_ids is None:
group_ids = [] group_ids = []
else: else:
@ -893,25 +895,30 @@ def etat_abs_date():
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
# Récupération des étudiants des groupes
etuds = [ etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members for m in groups_infos.members
] ]
# Récupération des assiduites des étudiants
assiduites: Assiduite = Assiduite.query.filter( assiduites: Assiduite = Assiduite.query.filter(
Assiduite.etudid.in_([e["etudid"] for e in etuds]) Assiduite.etudid.in_([e["etudid"] for e in etuds])
) )
# Filtrage des assiduités en fonction des dates données
assiduites = scass.filter_by_date( assiduites = scass.filter_by_date(
assiduites, Assiduite, date_debut, date_fin, False assiduites, Assiduite, date_debut, date_fin, False
) )
# Génération d'objet étudiant simplifié (nom+lien cal, etat_assiduite)
etudiants: list[dict] = [] etudiants: list[dict] = []
for etud in etuds: for etud in etuds:
# On récupère l'état de la première assiduité sur la période
assi = assiduites.filter_by(etudid=etud["etudid"]).first() assi = assiduites.filter_by(etudid=etud["etudid"]).first()
etat = "" etat = ""
if assi is not None and assi.etat != 0: if assi is not None and assi.etat != 0:
etat = scu.EtatAssiduite.inverse().get(assi.etat).name etat = scu.EtatAssiduite.inverse().get(assi.etat).name
# On génère l'objet simplifié
etudiant = { etudiant = {
"nom": f"""<a href="{url_for( "nom": f"""<a href="{url_for(
"assiduites.calendrier_etud", "assiduites.calendrier_etud",
@ -923,8 +930,10 @@ def etat_abs_date():
etudiants.append(etudiant) etudiants.append(etudiant)
# On tri les étudiants
etudiants = list(sorted(etudiants, key=lambda x: x["nom"])) etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
# Génération de l'HTML
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title=safehtml.html_to_safe_html(title), page_title=safehtml.html_to_safe_html(title),
init_qtip=True, init_qtip=True,
@ -948,6 +957,8 @@ def etat_abs_date():
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def visu_assi_group(): def visu_assi_group():
"""Visualisation de l'assiduité d'un groupe entre deux dates""" """Visualisation de l'assiduité d'un groupe entre deux dates"""
# Récupération des paramètres de la requête
dates = { dates = {
"debut": request.args.get("date_debut"), "debut": request.args.get("date_debut"),
"fin": request.args.get("date_fin"), "fin": request.args.get("date_fin"),
@ -961,13 +972,17 @@ def visu_assi_group():
group_ids = group_ids.split(",") group_ids = group_ids.split(",")
map(str, group_ids) map(str, group_ids)
# Récupération des groupes, du semestre et des étudiants
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
formsemestre = db.session.get(FormSemestre, groups_infos.formsemestre_id) formsemestre = db.session.get(FormSemestre, groups_infos.formsemestre_id)
etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members]) etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])
# Génération du tableau des assiduités
table: TableAssi = TableAssi( table: TableAssi = TableAssi(
etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre
) )
# Export en XLS
if fmt.startswith("xls"): if fmt.startswith("xls"):
return scu.send_file( return scu.send_file(
table.excel(), table.excel(),
@ -976,6 +991,7 @@ def visu_assi_group():
suffix=scu.XLSX_SUFFIX, suffix=scu.XLSX_SUFFIX,
) )
# récupération du/des noms du/des groupes
if groups_infos.tous_les_etuds_du_sem: if groups_infos.tous_les_etuds_du_sem:
gr_tit = "" gr_tit = ""
grp = "" grp = ""
@ -988,6 +1004,7 @@ def visu_assi_group():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>" grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
) )
# Génération de la page
return render_template( return render_template(
"assiduites/pages/visu_assi.j2", "assiduites/pages/visu_assi.j2",
assi_metric=scu.translate_assiduites_metric( assi_metric=scu.translate_assiduites_metric(
@ -1040,26 +1057,27 @@ def test():
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def signal_assiduites_diff(): def signal_assiduites_diff():
# Récupération des paramètres de la requête
group_ids: list[int] = request.args.get("group_ids", None) group_ids: list[int] = request.args.get("group_ids", None)
formsemestre_id: int = request.args.get("formsemestre_id", -1) formsemestre_id: int = request.args.get("formsemestre_id", -1)
date: str = request.args.get("jour", datetime.date.today().isoformat()) date: str = request.args.get("jour", datetime.date.today().isoformat())
date_deb: str = request.args.get("date_deb") date_deb: str = request.args.get("date_deb")
date_fin: str = request.args.get("date_fin") date_fin: str = request.args.get("date_fin")
semaine: str = request.args.get("semaine") semaine: str = request.args.get("semaine")
# Dans le cas où on donne une semaine plutot qu'un jour
if semaine is not None: if semaine is not None:
# On génère la semaine iso à partir de l'anne scolaire.
semaine = ( semaine = (
f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine f"{scu.annee_scolaire()}-W{semaine}" if "W" not in semaine else semaine
) )
# On met à jour les dates avec le date de debut et fin de semaine
date_deb: datetime.date = datetime.datetime.strptime( date_deb: datetime.date = datetime.datetime.strptime(
semaine + "-1", "%Y-W%W-%w" semaine + "-1", "%Y-W%W-%w"
) )
date_fin: datetime.date = date_deb + datetime.timedelta(days=6) date_fin: datetime.date = date_deb + datetime.timedelta(days=6)
etudiants: list[dict] = [] etudiants: list[dict] = []
titre = None titre = None
# Vérification du formsemestre_id # Vérification du formsemestre_id
@ -1079,14 +1097,13 @@ def signal_assiduites_diff():
elif real_date > formsemestre.date_fin: elif real_date > formsemestre.date_fin:
date = formsemestre.date_fin.isoformat() date = formsemestre.date_fin.isoformat()
# Vérification des groupes
if group_ids is None: if group_ids is None:
group_ids = [] group_ids = []
else: else:
group_ids = group_ids.split(",") group_ids = group_ids.split(",")
map(str, group_ids) map(str, group_ids)
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
if not groups_infos.members: if not groups_infos.members:
return ( return (
html_sco_header.sco_header(page_title="Assiduité: saisie différée") html_sco_header.sco_header(page_title="Assiduité: saisie différée")
@ -1094,6 +1111,7 @@ def signal_assiduites_diff():
+ html_sco_header.sco_footer() + html_sco_header.sco_footer()
) )
# Récupération des étudiants
etudiants.extend( etudiants.extend(
[ [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
@ -1103,6 +1121,8 @@ def signal_assiduites_diff():
etudiants = list(sorted(etudiants, key=lambda x: x["nom"])) etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
# Génération de l'HTML
header: str = html_sco_header.sco_header( header: str = html_sco_header.sco_header(
page_title="Assiduité: saisie différée", page_title="Assiduité: saisie différée",
init_qtip=True, init_qtip=True,
@ -1169,13 +1189,17 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
Alors l'absence sera sur la période de l'évaluation Alors l'absence sera sur la période de l'évaluation
Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant
""" """
# Récupération de l'étudiant concerné
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id: if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département") abort(404, "étudiant inexistant dans ce département")
# Récupération de l'évaluation concernée
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut
# Si l'évaluation dure plus qu'un jour alors on redirige vers la page de saisie etudiant
if delta > datetime.timedelta(days=1): if delta > datetime.timedelta(days=1):
# rediriger vers page saisie # rediriger vers page saisie
return redirect( return redirect(
@ -1191,7 +1215,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
) )
) )
# créer l'assiduité # Sinon on créé l'assiduité
try: try:
assiduite_unique: Assiduite = Assiduite.create_assiduite( assiduite_unique: Assiduite = Assiduite.create_assiduite(
@ -1202,8 +1226,8 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
moduleimpl=evaluation.moduleimpl, moduleimpl=evaluation.moduleimpl,
) )
except ScoValueError as see: except ScoValueError as see:
# En cas d'erreur
msg: str = see.args[0] msg: str = see.args[0]
if "Duplication" in msg: if "Duplication" in msg:
msg = "Une autre assiduité concerne déjà cette période. En cliquant sur continuer vous serez redirigé vers la page de saisie des assiduités de l'étudiant." msg = "Une autre assiduité concerne déjà cette période. En cliquant sur continuer vous serez redirigé vers la page de saisie des assiduités de l'étudiant."
dest: str = url_for( dest: str = url_for(
@ -1222,6 +1246,7 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
db.session.add(assiduite_unique) db.session.add(assiduite_unique)
db.session.commit() db.session.commit()
# on flash pour indiquer que l'absence a bien été créée puis on revient sur la page de l'évaluation
flash("L'absence a bien été créée") flash("L'absence a bien été créée")
# rediriger vers la page d'évaluation # rediriger vers la page d'évaluation
return redirect( return redirect(
@ -1235,16 +1260,20 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
"""Génère la liste des assiduités d'un étudiant pour le bulletin mail""" """Génère la liste des assiduités d'un étudiant pour le bulletin mail"""
# On récupère la métrique d'assiduité
metrique: str = scu.translate_assiduites_metric( metrique: str = scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", formsemestre_id=semestre.id), sco_preferences.get_preference("assi_metrique", formsemestre_id=semestre.id),
) )
# On récupère le nombre maximum de ligne d'assiduité
max_nb: int = int( max_nb: int = int(
sco_preferences.get_preference( sco_preferences.get_preference(
"bul_mail_list_abs_nb", formsemestre_id=semestre.id "bul_mail_list_abs_nb", formsemestre_id=semestre.id
) )
) )
# On récupère les assiduités et les justificatifs de l'étudiant
assiduites = scass.filter_by_formsemestre( assiduites = scass.filter_by_formsemestre(
etud.assiduites, Assiduite, semestre etud.assiduites, Assiduite, semestre
).order_by(Assiduite.entry_date.desc()) ).order_by(Assiduite.entry_date.desc())
@ -1252,10 +1281,17 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
etud.justificatifs, Justificatif, semestre etud.justificatifs, Justificatif, semestre
).order_by(Justificatif.entry_date.desc()) ).order_by(Justificatif.entry_date.desc())
# On calcule les statistiques
stats: dict = scass.get_assiduites_stats( stats: dict = scass.get_assiduites_stats(
assiduites, metric=metrique, filtered={"split": True} assiduites, metric=metrique, filtered={"split": True}
) )
# On sépare :
# - abs_j = absences justifiées
# - abs_nj = absences non justifiées
# - retards = les retards
# - justifs = les justificatifs
abs_j: list[str] = [ abs_j: list[str] = [
{"date": _get_date_str(assi.date_debut, assi.date_fin)} {"date": _get_date_str(assi.date_debut, assi.date_fin)}
for assi in assiduites for assi in assiduites
@ -1302,13 +1338,36 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str: def _get_date_str(deb: datetime.datetime, fin: datetime.datetime) -> str:
"""
_get_date_str transforme une période en chaîne lisible
Args:
deb (datetime.datetime): date de début
fin (datetime.datetime): date de fin
Returns:
str:
"le dd/mm/yyyy de hh:MM à hh:MM" si les deux date sont sur le même jour
"du dd/mm/yyyy hh:MM audd/mm/yyyy hh:MM" sinon
"""
if deb.date() == fin.date(): if deb.date() == fin.date():
temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime("%H:%M")] temps = deb.strftime("%d/%m/%Y %H:%M").split(" ") + [fin.strftime("%H:%M")]
return f"le {temps[0]} de {temps[1]} à {temps[2]}" return f"le {temps[0]} de {temps[1]} à {temps[2]}"
return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}' return f'du {deb.strftime("%d/%m/%Y %H:%M")} au {fin.strftime("%d/%m/%Y %H:%M")}'
def _get_days_between_dates(deb: str, fin: str): def _get_days_between_dates(deb: str, fin: str) -> str:
"""
_get_days_between_dates récupère tous les jours entre deux dates
Args:
deb (str): date de début
fin (str): date de fin
Returns:
str: une chaine json représentant une liste des jours
['date_iso','date_iso2', ...]
"""
if deb is None or fin is None: if deb is None or fin is None:
return "null" return "null"
try: try:
@ -1328,8 +1387,25 @@ def _get_days_between_dates(deb: str, fin: str):
def _differee( def _differee(
etudiants, moduleimpl_select, date=None, periode=None, formsemestre_id=None etudiants: list[dict],
): moduleimpl_select: str,
date: str = None,
periode: dict[str, str] = None,
formsemestre_id: int = None,
) -> str:
"""
_differee Génère un tableau de saisie différé
Args:
etudiants (list[dict]): la liste des étudiants (représentés par des dictionnaires)
moduleimpl_select (str): l'html représentant le selecteur de module
date (str, optional): la première date à afficher. Defaults to None.
periode (dict[str, str], optional):La période par défaut de la première colonne. Defaults to None.
formsemestre_id (int, optional): l'id du semestre pour le selecteur de module. Defaults to None.
Returns:
str: le widget (html/css/js)
"""
if date is None: if date is None:
date = datetime.date.today().isoformat() date = datetime.date.today().isoformat()
@ -1356,9 +1432,7 @@ def _differee(
) )
def _module_selector( def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str:
formsemestre: FormSemestre, moduleimpl_id: int = None
) -> HTMLElement:
""" """
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre _module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
@ -1368,18 +1442,26 @@ def _module_selector(
Returns: Returns:
str: La représentation str d'un HTMLSelectElement str: La représentation str d'un HTMLSelectElement
""" """
# récupération des ues du semestre
ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = ntc.get_ues_stat_dict()
modimpls_list: list[dict] = [] modimpls_list: list[dict] = []
ues = ntc.get_ues_stat_dict()
for ue in ues: for ue in ues:
# Ajout des moduleimpl de chaque ue dans la liste des moduleimpls
modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"]) modimpls_list += ntc.get_modimpls_dict(ue_id=ue["ue_id"])
# prévoie la sélection par défaut d'un moduleimpl s'il a été passé en paramètre
selected = "" if moduleimpl_id is not None else "selected" selected = "" if moduleimpl_id is not None else "selected"
modules = [] # Vérification que le moduleimpl_id passé en paramètre est bien un entier
try:
moduleimpl_id = int(moduleimpl_id)
except (ValueError, TypeError):
moduleimpl_id = None
modules: list[dict[str, str | int]] = []
# Récupération de l'id et d'un nom lisible pour chaque moduleimpl
for modimpl in modimpls_list: for modimpl in modimpls_list:
modname: str = ( modname: str = (
(modimpl["module"]["code"] or "") (modimpl["module"]["code"] or "")
@ -1388,11 +1470,6 @@ def _module_selector(
) )
modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname}) modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})
try:
moduleimpl_id = int(moduleimpl_id)
except (ValueError, TypeError):
moduleimpl_id = None
return render_template( return render_template(
"assiduites/widgets/moduleimpl_selector.j2", "assiduites/widgets/moduleimpl_selector.j2",
selected=selected, selected=selected,
@ -1401,13 +1478,30 @@ def _module_selector(
) )
def _dynamic_module_selector(): def _dynamic_module_selector() -> str:
"""
_dynamic_module_selector retourne l'html/css/javascript du selecteur de module dynamique
Returns:
str: l'html/css/javascript du selecteur de module dynamique
"""
return render_template( return render_template(
"assiduites/widgets/moduleimpl_dynamic_selector.j2", "assiduites/widgets/moduleimpl_dynamic_selector.j2",
) )
def _timeline(formsemestre_id=None) -> HTMLElement: def _timeline(formsemestre_id: int = None) -> str:
"""
_timeline retourne l'html de la timeline
Args:
formsemestre_id (int, optional): un formsemestre. Defaults to None.
Le formsemestre sert à obtenir la période par défaut de la timeline
sinon ce sera de 2 heure dès le début de la timeline
Returns:
str: l'html en chaîne de caractères
"""
return render_template( return render_template(
"assiduites/widgets/timeline.j2", "assiduites/widgets/timeline.j2",
t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"), t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),
@ -1419,7 +1513,13 @@ def _timeline(formsemestre_id=None) -> HTMLElement:
) )
def _mini_timeline() -> HTMLElement: def _mini_timeline() -> str:
"""
_mini_timeline Retourne l'html lié au mini timeline d'assiduités
Returns:
str: l'html en chaîne de caractères
"""
return render_template( return render_template(
"assiduites/widgets/minitimeline.j2", "assiduites/widgets/minitimeline.j2",
t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"), t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.49" SCOVERSION = "9.6.50"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -26,6 +26,7 @@ def downgrade_module(
dept_etudid: list[int] = None dept_etudid: list[int] = None
dept_id: int = None dept_id: int = None
# Récupération du département si spécifié
if dept is not None: if dept is not None:
departement: Departement = Departement.query.filter_by(acronym=dept).first() departement: Departement = Departement.query.filter_by(acronym=dept).first()
@ -34,13 +35,16 @@ def downgrade_module(
dept_etudid = [etud.id for etud in departement.etudiants] dept_etudid = [etud.id for etud in departement.etudiants]
dept_id = departement.id dept_id = departement.id
# Suppression des assiduités
if assiduites: if assiduites:
_remove_assiduites(dept_etudid) _remove_assiduites(dept_etudid)
# Suppression des justificatifs
if justificatifs: if justificatifs:
_remove_justificatifs(dept_etudid) _remove_justificatifs(dept_etudid)
_remove_justificatifs_archive(dept_id) _remove_justificatifs_archive(dept_id)
# Si on supprime tout le module assiduité/justificatif alors on remet à zero
# les séquences postgres
if dept is None: if dept is None:
if assiduites: if assiduites:
db.session.execute( db.session.execute(
@ -51,26 +55,52 @@ def downgrade_module(
sa.text("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1") sa.text("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1")
) )
# On valide l'opération sur la bdd
db.session.commit() db.session.commit()
# On affiche un message pour l'utilisateur
print( print(
f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}" f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}"
) )
def _remove_assiduites(dept_etudid: str = None): def _remove_assiduites(dept_etudid: str = None):
"""
_remove_assiduites Supprime les assiduités
Args:
dept_etudid (str, optional): la liste des etudid d'un département. Defaults to None.
"""
if dept_etudid is None: if dept_etudid is None:
# Si pas d'étudids alors on supprime toutes les assiduités
Assiduite.query.delete() Assiduite.query.delete()
else: else:
# Sinon on supprime que les assiduités des étudiants donnés
Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete() Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete()
def _remove_justificatifs(dept_etudid: str = None): def _remove_justificatifs(dept_etudid: str = None):
"""
_remove_justificatifs Supprime les justificatifs
Args:
dept_etudid (str, optional): la liste des etudid d'un département. Defaults to None.
"""
if dept_etudid is None: if dept_etudid is None:
# Si pas d'étudids alors on supprime tous les justificatifs
Justificatif.query.delete() Justificatif.query.delete()
else: else:
# Sinon on supprime que les justificatifs des étudiants donnés
Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete() Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete()
def _remove_justificatifs_archive(dept_id: int = None): def _remove_justificatifs_archive(dept_id: int = None):
"""
_remove_justificatifs_archive On supprime les archives des fichiers justificatifs
Args:
dept_id (int, optional): l'id du département à supprimer . Defaults to None.
Si none : supprime tous les département
Sinon uniquement le département sélectionné
"""
JustificatifArchiver().remove_dept_archive(dept_id) JustificatifArchiver().remove_dept_archive(dept_id)