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)
# Vérification que l'étudiant existe
if etud is None:
return json_error(
404,
message="étudiant inconnu",
)
# Les filtres qui seront appliqués au comptage (type, date, etudid...)
filtered: dict[str, object] = {}
# la métrique du comptage (all, demi, heure, journee)
metric: str = "all"
# Si la requête a des paramètres
if with_query:
metric, filtered = _count_manager(request)
@ -254,11 +253,7 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
"""
# query = Identite.query.filter_by(id=etudid)
# if g.scodoc_dept:
# query = query.filter_by(dept_id=g.scodoc_dept_id)
# etud: Identite = query.first_or_404(etudid)
# Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine)
if etud is None:
@ -266,15 +261,23 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
404,
message="étudiant inconnu",
)
# Récupération des assiduités de l'étudiant
assiduites_query: Query = etud.assiduites
# Filtrage des assiduités en fonction des paramètres de la requête
if with_query:
assiduites_query = _filter_manager(request, assiduites_query)
# Préparation de la réponse json
data_set: list[dict] = []
for ass in assiduites_query.all():
# conversion Assiduite -> Dict
data = ass.to_dict(format_api=True)
# Ajout des justificatifs (ou non dépendamment de la requête)
data = _with_justifs(data)
# Ajout de l'assiduité dans la liste de retour
data_set.append(data)
return data_set
@ -326,6 +329,7 @@ def assiduites_group(with_query: bool = False):
"""
# Récupération des étudiants dans la requête
etuds = request.args.get("etudids", "")
etuds = etuds.split(",")
try:
@ -333,6 +337,7 @@ def assiduites_group(with_query: bool = False):
except ValueError:
return json_error(404, "Le champs etudids n'est pas correctement formé")
# Vérification que tous les étudiants sont du même département
query = Identite.query.filter(Identite.id.in_(etuds))
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
@ -342,15 +347,21 @@ def assiduites_group(with_query: bool = False):
404,
"Tous les étudiants ne sont pas dans le même département et/ou n'existe pas.",
)
# Récupération de toutes les assiduités liés aux étudiants
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds))
# Filtrage des assiduités en fonction des filtres passés dans la requête
if with_query:
assiduites_query = _filter_manager(request, assiduites_query)
# Préparation de retour json
# Dict représentant chaque étudiant avec sa liste d'assiduité
data_set: dict[list[dict]] = {str(key): [] for key in etuds}
for ass in assiduites_query.all():
data = ass.to_dict(format_api=True)
data = _with_justifs(data)
# Ajout de l'assiduité dans la liste du bon étudiant
data_set.get(str(data["etudid"])).append(data)
return data_set
@ -375,20 +386,23 @@ def assiduites_group(with_query: bool = False):
@permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne toutes les assiduités du formsemestre"""
# Récupération du formsemestre à partir du formsemestre_id
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
# Récupération des assiduités du formsemestre
assiduites_query = scass.filter_by_formsemestre(
Assiduite.query, Assiduite, formsemestre
)
# Filtrage en fonction des paramètres de la requête
if with_query:
assiduites_query = _filter_manager(request, assiduites_query)
# Préparation du retour JSON
data_set: list[dict] = []
for ass in assiduites_query.all():
data = ass.to_dict(format_api=True)
@ -422,21 +436,28 @@ def count_assiduites_formsemestre(
formsemestre_id: int = None, with_query: bool = False
):
"""Comptage des assiduités du formsemestre"""
# Récupération du formsemestre à partir du formsemestre_id
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
# Récupération des étudiants du formsemestre
etuds = formsemestre.etuds.all()
etuds_id = [etud.id for etud in etuds]
# Récupération des assiduités des étudiants du semestre
assiduites_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
# Filtrage des assiduités en fonction des dates du semestre
assiduites_query = scass.filter_by_formsemestre(
assiduites_query, Assiduite, formsemestre
)
# Gestion de la métrique de comptage (all,demi,heure,journee)
metric: str = "all"
# Gestion du filtre (en fonction des paramètres de la requête)
filtered: dict = {}
if with_query:
metric, filtered = _count_manager(request)
@ -481,23 +502,36 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
]
"""
# Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine)
if etud is None:
return json_error(
404,
message="étudiant inconnu",
)
# Mise à jour du "g.scodoc_dept" si route sans dept
if g.scodoc_dept is None and etud.dept_id is not None:
# route sans département
set_sco_dept(etud.departement.acronym)
# Récupération de la liste des assiduités à créer
create_list: list[object] = request.get_json(force=True)
# Vérification que c'est bien une liste
if not isinstance(create_list, list):
return json_error(404, "Le contenu envoyé n'est pas une liste")
errors: list = []
success: list = []
# Préparation du retour
errors: list[dict[str, object]] = []
success: list[dict[str, object]] = []
# Pour chaque objet de la liste,
# on récupère son indice et l'objet
for i, data in enumerate(create_list):
# On créé l'assiduité
# 200 + obj si réussi
# 404 + message d'erreur si non réussi
code, obj = _create_one(data, etud)
if code == 404:
errors.append({"indice": i, "message": obj})
@ -570,53 +604,77 @@ def _create_one(
data: dict,
etud: Identite,
) -> tuple[int, object]:
"""TODO @iziram: documenter"""
"""
_create_one Création d'une assiduité à partir d'une représentation JSON
Cette fonction vérifie la représentation JSON
Puis crée l'assiduité si la représentation est valide.
Args:
data (dict): représentation json d'une assiduité
etud (Identite): l'étudiant concerné par l'assiduité
Returns:
tuple[int, object]: code, objet
code : 200 si réussi 404 sinon
objet : dict{assiduite_id:?} si réussi str"message d'erreur" sinon
"""
errors: list[str] = []
# -- vérifications de l'objet json --
# cas 1 : ETAT
etat = data.get("etat", None)
etat: str = data.get("etat", None)
if etat is None:
errors.append("param 'etat': manquant")
elif not scu.EtatAssiduite.contains(etat):
errors.append("param 'etat': invalide")
etat = scu.EtatAssiduite.get(etat)
etat: scu.EtatAssiduite = scu.EtatAssiduite.get(etat)
# cas 2 : date_debut
date_debut = data.get("date_debut", None)
date_debut: str = data.get("date_debut", None)
if date_debut is None:
errors.append("param 'date_debut': manquant")
deb = scu.is_iso_formated(date_debut, convert=True)
# Conversion de la chaine de caractère en datetime (tz ou non)
deb: datetime = scu.is_iso_formated(date_debut, convert=True)
# si chaine invalide
if deb is None:
errors.append("param 'date_debut': format invalide")
# Si datetime sans timezone
elif deb.tzinfo is None:
deb = scu.localize_datetime(deb)
# Mise à jour de la timezone avec celle du serveur
deb: datetime = scu.localize_datetime(deb)
# cas 3 : date_fin
date_fin = data.get("date_fin", None)
# cas 3 : date_fin (Même fonctionnement ^ )
date_fin: str = data.get("date_fin", None)
if date_fin is None:
errors.append("param 'date_fin': manquant")
fin = scu.is_iso_formated(date_fin, convert=True)
fin: datetime = scu.is_iso_formated(date_fin, convert=True)
if fin is None:
errors.append("param 'date_fin': format invalide")
elif fin.tzinfo is None:
fin = scu.localize_datetime(fin)
fin: datetime = scu.localize_datetime(fin)
# cas 5 : desc
# cas 4 : desc
desc: str = data.get("desc", None)
external_data = data.get("external_data", None)
# cas 5 : external data
external_data: dict = data.get("external_data", None)
if external_data is not None:
if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON")
# cas 4 : moduleimpl_id
# cas 6 : moduleimpl_id
# On récupère le moduleimpl
moduleimpl_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None
# On vérifie si le moduleimpl existe (uniquement s'il a été renseigné)
if moduleimpl_id not in [False, None, "", "-1"]:
# Si le moduleimpl n'est pas "autre" alors on vérifie si l'id est valide
if moduleimpl_id != "autre":
try:
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
@ -625,16 +683,23 @@ def _create_one(
if moduleimpl is None:
errors.append("param 'moduleimpl_id': invalide")
else:
# Sinon on met à none le moduleimpl
# et on ajoute dans external data
# le module "autre"
moduleimpl_id = None
external_data = external_data if external_data is not None else {}
external_data: dict = external_data if external_data is not None else {}
external_data["module"] = "Autre"
# Si il y a des erreurs alors on ne crée pas l'assiduité et on renvoie les erreurs
if errors:
# Construit une chaine de caractère avec les erreurs séparées par `,`
err: str = ", ".join(errors)
# 404 représente le code d'erreur et err la chaine nouvellement créée
return (404, err)
# TOUT EST OK
# SI TOUT EST OK
try:
# On essaye de créer l'assiduité
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
date_debut=deb,
date_fin=fin,
@ -647,12 +712,23 @@ def _create_one(
notify_mail=True,
)
# create_assiduite générera des ScoValueError si jamais il y a un autre problème
# - Etudiant non inscrit dans le module
# - module obligatoire
# - Assiduité conflictuelles
# Si tout s'est bien passé on ajoute l'assiduité à la session
# et on retourne un code 200 avec un objet possèdant l'assiduite_id
db.session.add(nouv_assiduite)
db.session.commit()
return (200, {"assiduite_id": nouv_assiduite.id})
except ScoValueError as excp:
# ici on utilise pas json_error car on doit renvoyer status, message
# Ici json_error ne peut être utilisé car il terminerai le processus de création
# Cela voudrait dire qu'une seule erreur dans une assiduité imposerait de
# tout refaire à partir de l'erreur.
# renvoit un code 404 et le message d'erreur de la ScoValueError
return 404, excp.args[0]
@ -675,14 +751,20 @@ def assiduite_delete():
"""
# Récupération des ids envoyés dans la liste
assiduites_list: list[int] = request.get_json(force=True)
if not isinstance(assiduites_list, list):
return json_error(404, "Le contenu envoyé n'est pas une liste")
# Préparation du retour json
output = {"errors": [], "success": []}
for i, ass in enumerate(assiduites_list):
code, msg = _delete_singular(ass, db)
# Pour chaque assiduite_id on essaye de supprimer l'assiduité
for i, assiduite_id in enumerate(assiduites_list):
# De la même façon que "_create_one"
# Ici le code est soit 200 si réussi ou 404 si raté
# Le message est le message d'erreur si erreur
code, msg = _delete_one(assiduite_id)
if code == 404:
output["errors"].append({"indice": i, "message": msg})
else:
@ -692,24 +774,43 @@ def assiduite_delete():
return output
def _delete_singular(assiduite_id: int, database) -> tuple[int, str]:
"""@iziram PLEASE COMMENT THIS F*CKING CODE"""
def _delete_one(assiduite_id: int) -> tuple[int, str]:
"""
_delete_singular Supprime une assiduité à partir de son id
Args:
assiduite_id (int): l'identifiant de l'assiduité
Returns:
tuple[int, str]: code, message
code : 200 si réussi, 404 sinon
message : OK si réussi, le message d'erreur sinon
"""
assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
if assiduite_unique is None:
# on ne peut pas utiliser json_error ici car on est déclaré (int, str)
# Ici json_error ne peut être utilisé car il terminerai le processus de création
# Cela voudrait dire qu'une seule erreur d'id imposerait de
# tout refaire à partir de l'erreur.
return 404, "Assiduite non existante"
# Mise à jour du g.scodoc_dept si la route est sans département
if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None:
# route sans département
set_sco_dept(assiduite_unique.etudiant.departement.acronym)
ass_dict = assiduite_unique.to_dict()
# Récupération de la version dict de l'assiduité
# Pour invalider le cache
assi_dict = assiduite_unique.to_dict()
# Suppression de l'assiduité et LOG
log(f"delete_assiduite: {assiduite_unique.etudiant.id} {assiduite_unique}")
Scolog.logdb(
method="delete_assiduite",
etudid=assiduite_unique.etudiant.id,
msg=f"assiduité: {assiduite_unique}",
)
database.session.delete(assiduite_unique)
scass.simple_invalidate_cache(ass_dict)
db.session.delete(assiduite_unique)
# Invalidation du cache
scass.simple_invalidate_cache(assi_dict)
return 200, "OK"
@ -730,17 +831,25 @@ def assiduite_edit(assiduite_id: int):
"est_just"?: bool
}
"""
# Récupération de l'assiduité à modifier
assiduite_unique: Assiduite = Assiduite.query.filter_by(
id=assiduite_id
).first_or_404()
errors: list[str] = []
# Récupération des valeurs à modifier
data = request.get_json(force=True)
code, obj = _edit_singular(assiduite_unique, data)
# Préparation du retour
errors: list[str] = []
# Code 200 si modification réussie
# Code 404 si raté + message d'erreur
code, obj = _edit_one(assiduite_unique, data)
if code == 404:
return json_error(404, obj)
# Mise à jour de l'assiduité et LOG
log(f"assiduite_edit: {assiduite_unique.etudiant.id} {assiduite_unique}")
Scolog.logdb(
"assiduite_edit",
@ -791,7 +900,7 @@ def assiduites_edit():
)
continue
code, obj = _edit_singular(assi, data)
code, obj = _edit_one(assi, data)
obj_retour = {
"indice": i,
"message": obj,
@ -806,46 +915,69 @@ def assiduites_edit():
return {"errors": errors, "success": success}
def _edit_singular(assiduite_unique, data):
def _edit_one(assiduite_unique: Assiduite, data: dict) -> tuple[int, str]:
"""
_edit_singular Modifie une assiduité à partir de données JSON
Args:
assiduite_unique (Assiduite): l'assiduité à modifier
data (dict): les nouvelles données
Returns:
tuple[int,str]: code, message
code : 200 si réussi, 404 sinon
message : OK si réussi, message d'erreur sinon
"""
# Mise à jour du g.scodoc_dept en cas de route sans département
if g.scodoc_dept is None and assiduite_unique.etudiant.dept_id is not None:
# route sans département
set_sco_dept(assiduite_unique.etudiant.departement.acronym)
errors: list[str] = []
# Vérifications de data
# Cas 1 : Etat
if data.get("etat") is not None:
etat = scu.EtatAssiduite.get(data.get("etat"))
etat: scu.EtatAssiduite = scu.EtatAssiduite.get(data.get("etat"))
if etat is None:
errors.append("param 'etat': invalide")
else:
# Mise à jour de l'état
assiduite_unique.etat = etat
external_data = data.get("external_data")
# Cas 2 : external_data
external_data: dict = data.get("external_data")
if external_data is not None:
if not isinstance(external_data, dict):
errors.append("param 'external_data' : n'est pas un objet JSON")
else:
# Mise à jour de l'external data
assiduite_unique.external_data = external_data
# Cas 2 : Moduleimpl_id
# Cas 3 : Moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None
# False si on modifie pas le moduleimpl
if moduleimpl_id is not False:
# Si le module n'est pas nul
if moduleimpl_id not in [None, "", "-1"]:
# Gestion du module Autre
if moduleimpl_id == "autre":
# module autre = moduleimpl_id:None + external_data["module"]:"Autre"
assiduite_unique.moduleimpl_id = None
external_data = (
external_data: dict = (
external_data
if external_data is not None and isinstance(external_data, dict)
else assiduite_unique.external_data
)
external_data = external_data if external_data is not None else {}
external_data: dict = external_data if external_data is not None else {}
external_data["module"] = "Autre"
assiduite_unique.external_data = external_data
else:
# Vérification de l'id et récupération de l'objet ModuleImpl
try:
moduleimpl = ModuleImpl.query.filter_by(
id=int(moduleimpl_id)
@ -861,8 +993,11 @@ def _edit_singular(assiduite_unique, data):
):
errors.append("param 'moduleimpl_id': etud non inscrit")
else:
# Mise à jour du moduleimpl
assiduite_unique.moduleimpl_id = moduleimpl_id
else:
# Vérification du force module en cas de modification du moduleimpl en moduleimpl nul
# Récupération du formsemestre lié à l'assiduité
formsemestre: FormSemestre = get_formsemestre_from_data(
assiduite_unique.to_dict()
)
@ -873,17 +1008,23 @@ def _edit_singular(assiduite_unique, data):
else:
force = scu.is_assiduites_module_forced(dept_id=etud.dept_id)
if force:
external_data = (
external_data
if external_data is not None and isinstance(external_data, dict)
else assiduite_unique.external_data
)
if force and not external_data.get("module", False):
errors.append(
"param 'moduleimpl_id' : le moduleimpl_id ne peut pas être nul"
)
# Cas 3 : desc
desc = data.get("desc", False)
# Cas 4 : desc
desc: str = data.get("desc", False)
if desc is not False:
assiduite_unique.description = desc
# Cas 4 : est_just
# Cas 5 : est_just
if assiduite_unique.etat == scu.EtatAssiduite.PRESENT:
assiduite_unique.est_just = False
else:
@ -900,9 +1041,11 @@ def _edit_singular(assiduite_unique, data):
)
if errors:
# Retour des erreurs en une seule chaîne séparée par des `,`
err: str = ", ".join(errors)
return (404, err)
# Mise à jour de l'assiduité et LOG
log(f"_edit_singular: {assiduite_unique.etudiant.id} {assiduite_unique}")
Scolog.logdb(
"assiduite_edit",
@ -997,21 +1140,34 @@ def _count_manager(requested) -> tuple[str, dict]:
def _filter_manager(requested, assiduites_query: Query) -> Query:
"""
Retourne les assiduites entrées filtrées en fonction de la request
_filter_manager Retourne les assiduites entrées filtrées en fonction de la request
Args:
requested (request): La requête http
assiduites_query (Query): la query d'assiduités à filtrer
Returns:
Query: La query filtrée
"""
# cas 1 : etat assiduite
etat = requested.args.get("etat")
etat: str = requested.args.get("etat")
if etat is not None:
assiduites_query = scass.filter_assiduites_by_etat(assiduites_query, etat)
# cas 2 : date de début
deb = requested.args.get("date_debut", "").replace(" ", "+")
deb: datetime = scu.is_iso_formated(deb, True)
deb: str = requested.args.get("date_debut", "").replace(" ", "+")
deb: datetime = scu.is_iso_formated(
deb, True
) # transformation de la chaine en datetime
# cas 3 : date de fin
fin = requested.args.get("date_fin", "").replace(" ", "+")
fin = scu.is_iso_formated(fin, True)
fin: str = requested.args.get("date_fin", "").replace(" ", "+")
fin: datetime = scu.is_iso_formated(
fin, True
) # transformation de la chaine en datetime
# Pour filtrer les dates il faut forcement avoir les deux bornes
# [date_debut : date_fin]
if (deb, fin) != (None, None):
assiduites_query: Query = scass.filter_by_date(
assiduites_query, Assiduite, deb, fin
@ -1065,10 +1221,12 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
if user_id is not False:
assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id)
# cas 9 : order (renvoie la query ordonnée en "date début Décroissante")
order = requested.args.get("order", None)
if order is not None:
assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc())
# cas 10 : courant (Ne renvoie que les assiduités de l'année courante)
courant = requested.args.get("courant", None)
if courant is not None:
annee: int = scu.annee_scolaire()
@ -1081,7 +1239,17 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
return assiduites_query
def _with_justifs(assi):
def _with_justifs(assi: dict):
"""
_with_justifs ajoute la liste des justificatifs à l'assiduité
Condition : `with_justifs` doit se trouver dans les paramètres de la requête
Args:
assi (dict): un dictionnaire représentant une assiduité
Returns:
dict: l'assiduité avec les justificatifs ajoutés
"""
if request.args.get("with_justifs") is None:
return assi
assi["justificatifs"] = get_assiduites_justif(assi["assiduite_id"], True)

View File

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

View File

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

View File

@ -82,6 +82,7 @@ class Assiduite(db.Model):
etat = self.etat
user: User = None
if format_api:
# format api utilise les noms "present,absent,retard" au lieu des int
etat = EtatAssiduite.inverse().get(self.etat).name
if self.user_id is not None:
user = db.session.get(User, self.user_id)
@ -345,15 +346,10 @@ def is_period_conflicting(
avec les justificatifs ou assiduites déjà présentes
"""
# On s'assure que les dates soient avec TimeZone
date_debut = localize_datetime(date_debut)
date_fin = localize_datetime(date_fin)
if (
collection.filter_by(date_debut=date_debut, date_fin=date_fin).first()
is not None
):
return True
count: int = collection.filter(
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
).count()
@ -375,19 +371,26 @@ def compute_assiduites_justified(
Returns:
list[int]: la liste des assiduités qui ont été justifiées.
"""
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
if justificatifs is None:
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all()
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
etudid=etudid
).all()
# On ne prend que les justificatifs valides
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
# On récupère les assiduités de l'étudiant
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites_justifiees: list[int] = []
for assi in assiduites:
# On ne justifie pas les Présences
if assi.etat == EtatAssiduite.PRESENT:
continue
# On récupère les justificatifs qui justifient l'assiduité `assi`
assi_justificatifs = Justificatif.query.filter(
Justificatif.etudid == assi.etudid,
Justificatif.date_debut <= assi.date_debut,
@ -395,21 +398,39 @@ def compute_assiduites_justified(
Justificatif.etat == EtatJustificatif.VALIDE,
).all()
# Si au moins un justificatif possède une période qui couvre l'assiduité
if any(
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
for j in justificatifs + assi_justificatifs
):
# On justifie l'assiduité
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id)
db.session.add(assi)
elif reset:
# Si le paramètre reset est Vrai alors les assiduités non justifiées
# sont remise en "non justifiée"
assi.est_just = False
db.session.add(assi)
# On valide la session
db.session.commit()
# On renvoie la liste des assiduite_id des assiduités justifiées
return assiduites_justifiees
def get_assiduites_justif(assiduite_id: int, long: bool):
def get_assiduites_justif(assiduite_id: int, long: bool) -> list[int | dict]:
"""
get_assiduites_justif Récupération des justificatifs d'une assiduité
Args:
assiduite_id (int): l'identifiant de l'assiduité
long (bool): Retourner des dictionnaires à la place
des identifiants des justificatifs
Returns:
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
"""
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
@ -420,20 +441,57 @@ def get_justifs_from_date(
date_fin: datetime,
long: bool = False,
valid: bool = False,
):
) -> list[int | dict]:
"""
get_justifs_from_date Récupération des justificatifs couvrant une période pour un étudiant donné
Args:
etudid (int): l'identifiant de l'étudiant
date_debut (datetime): la date de début (datetime avec timezone)
date_fin (datetime): la date de fin (datetime avec timezone)
long (bool, optional): Définition de la sortie.
Vrai pour avoir les dictionnaires des justificatifs.
Faux pour avoir uniquement les identifiants
Defaults to False.
valid (bool, optional): Filtre pour n'avoir que les justificatifs valide.
Si vrai : le retour ne contiendra que des justificatifs valides
Sinon le retour contiendra tout type de justificatifs
Defaults to False.
Returns:
list[int | dict]: La liste des justificatifs (par défaut uniquement les identifiants, sinon les Dict si long est vrai)
"""
# On récupère les justificatifs d'un étudiant couvrant la période donnée
justifs: Query = Justificatif.query.filter(
Justificatif.etudid == etudid,
Justificatif.date_debut <= date_debut,
Justificatif.date_fin >= date_fin,
)
# si valide est vrai alors on filtre pour n'avoir que les justificatifs valide
if valid:
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
# On renvoie la liste des id des justificatifs si long est Faux, sinon on renvoie les dicts des justificatifs
return [j.justif_id if not long else j.to_dict(True) for j in justifs]
def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
"""
get_formsemestre_from_data récupère un formsemestre en fonction des données passées
Args:
data (dict[str, datetime | int]): Une réprésentation simplifiée d'une assiduité ou d'un justificatif
data = {
"etudid" : int,
"date_debut": datetime (tz),
"date_fin": datetime (tz),
}
Returns:
FormSemestre: Le formsemestre trouvé ou None
"""
return (
FormSemestre.query.join(
FormSemestreInscription,

View File

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

View File

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

View File

@ -160,6 +160,8 @@ class HTMLBuilder:
@permission_required(Permission.AbsChange)
def bilan_dept():
"""Gestionnaire assiduités, page principale"""
# Préparation de la page
H = [
html_sco_header.sco_header(
page_title="Saisie de l'assiduité",
@ -183,7 +185,10 @@ def bilan_dept():
"""<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
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"))
# Gestion des billets d'absences
if current_user.has_permission(
Permission.AbsChange
) and sco_preferences.get_preference("handle_billets_abs"):
@ -195,19 +200,23 @@ def bilan_dept():
</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()
annees: list[int] = sorted(
[f.date_debut.year for f in dept.formsemestres],
reverse=True,
)
annee = scu.annee_scolaire()
annee = scu.annee_scolaire() # Année courante, sera utilisée par défaut
# Génération d'une liste "json" d'années
annees_str: str = "["
for ann in annees:
annees_str += f"{ann},"
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", "")
if formsemestre_id:
try:
@ -216,6 +225,7 @@ def bilan_dept():
except AttributeError:
formsemestre_id = ""
# Peuplement du template jinja
H.append(
render_template(
"assiduites/pages/bilan_dept.j2",
@ -230,49 +240,6 @@ def bilan_dept():
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")
@scodoc
@permission_required(Permission.AbsChange)
@ -287,22 +254,22 @@ def signal_assiduites_etud():
str: l'html généré
"""
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
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())
# gestion évaluations
# gestion évaluations (Appel à la page depuis les évaluations)
saisie_eval: bool = request.args.get("saisie_eval") is not None
date_deb: str = request.args.get("date_deb")
date_fin: str = request.args.get("date_fin")
moduleimpl_id: int = request.args.get("moduleimpl_id", "")
evaluation_id: int = request.args.get("evaluation_id")
redirect_url: str = (
"#"
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(
page_title="Saisie assiduité",
init_qtip=True,
@ -335,11 +303,14 @@ def signal_assiduites_etud():
"assi_afternoon_time", "18:00:00"
)
# Gestion du selecteur de moduleimpl (pour le tableau différé)
select = f"""
<select class="dynaSelect">
{render_template("assiduites/widgets/simplemoduleimpl_select.j2")}
</select>
"""
# Génération de la page
return HTMLBuilder(
header,
_mini_timeline(),
@ -381,13 +352,16 @@ def liste_assiduites_etud():
str: l'html généré
"""
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
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)
# Préparation de la page
header: str = html_sco_header.sco_header(
page_title=f"Assiduité de {etud.nomprenom}",
init_qtip=True,
@ -401,7 +375,7 @@ def liste_assiduites_etud():
"css/assiduites.css",
],
)
# Peuplement du template jinja
return HTMLBuilder(
header,
render_template(
@ -429,12 +403,13 @@ def bilan_etud():
Returns:
str: l'html généré
"""
# Récupération de l'étudiant
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
# Préparation de la page (header)
header: str = html_sco_header.sco_header(
page_title=f"Bilan de l'assiduité de {etud.nomprenom}",
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_fin: str = f"{scu.annee_scolaire()+1}-06-30"
# Récupération de la métrique d'assiduité
assi_metric = scu.translate_assiduites_metric(
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
)
# Génération de la page
return HTMLBuilder(
header,
render_template(
@ -486,11 +464,13 @@ def ajout_justificatif_etud():
str: l'html généré
"""
# Récupération de l'étudiant concerné
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
# Préparation de la page (header)
header: str = html_sco_header.sco_header(
page_title="Justificatifs",
init_qtip=True,
@ -505,6 +485,7 @@ def ajout_justificatif_etud():
],
)
# Peuplement du template jinja
return HTMLBuilder(
header,
render_template(
@ -533,11 +514,13 @@ def calendrier_etud():
str: l'html généré
"""
# Récupération de l'étudiant
etudid = request.args.get("etudid", -1)
etud: Identite = Identite.query.get_or_404(etudid)
if etud.dept_id != g.scodoc_dept_id:
abort(404, "étudiant inexistant dans ce département")
# Préparation de la page
header: str = html_sco_header.sco_header(
page_title="Calendrier de l'assiduité",
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(
[ins.formsemestre.date_debut.year for ins in etud.formsemestre_inscriptions],
reverse=True,
)
# Transformation en une liste "json"
# (sera utilisé pour générer le selecteur d'année)
annees_str: str = "["
for ann in annees:
annees_str += f"{ann},"
annees_str += "]"
# Peuplement du template jinja
return HTMLBuilder(
header,
render_template(
@ -585,11 +572,11 @@ def signal_assiduites_group():
Returns:
str: l'html généré
"""
# Récupération des paramètres de l'url
formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat())
group_ids: list[int] = request.args.get("group_ids", None)
if group_ids is None:
group_ids = []
else:
@ -601,12 +588,14 @@ def signal_assiduites_group():
moduleimpl_id = int(moduleimpl_id)
except (TypeError, ValueError):
moduleimpl_id = None
# Vérification du formsemestre_id
try:
formsemestre_id = int(formsemestre_id)
except (TypeError, ValueError):
formsemestre_id = None
# Gestion des groupes
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
)
@ -617,10 +606,6 @@ def signal_assiduites_group():
+ html_sco_header.sco_footer()
)
# --- URL DEFAULT ---
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
# --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id
@ -628,17 +613,22 @@ def signal_assiduites_group():
if formsemestre.dept_id != g.scodoc_dept_id:
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)
# Récupération des étudiants des groupes
etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
]
# --- Vérification de la date ---
real_date = scu.is_iso_formated(date, True).date()
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")
form_deb = formsemestre.date_debut.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...
moduleimpl_id = None
# --- Génération de l'HTML ---
sem = formsemestre.to_dict()
# Récupération du nom des/du groupe(s)
if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en"
@ -676,12 +665,15 @@ def signal_assiduites_group():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
# --- Génération de l'HTML ---
header: str = html_sco_header.sco_header(
page_title="Saisie journalière des assiduités",
init_qtip=True,
javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS
+ [
# Voir fonctionnement JS
# XXX Retirer moment
"js/etud_info.js",
"js/groups_view.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(
header,
_mini_timeline(),
@ -731,11 +727,12 @@ def visu_assiduites_group():
Returns:
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)
moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat())
group_ids: list[int] = request.args.get("group_ids", None)
if group_ids is None:
group_ids = []
else:
@ -755,6 +752,7 @@ def visu_assiduites_group():
except (TypeError, ValueError) as exc:
raise ScoValueError("identifiant de formsemestre invalide") from exc
# Récupérations des/du groupe(s)
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id
)
@ -765,10 +763,6 @@ def visu_assiduites_group():
+ html_sco_header.sco_footer()
)
# --- URL DEFAULT ---
base_url: str = f"SignalAssiduiteGr?date={date}&{groups_infos.groups_query_args}"
# --- Filtrage par formsemestre ---
formsemestre_id = groups_infos.formsemestre_id
@ -776,10 +770,10 @@ def visu_assiduites_group():
if formsemestre.dept_id != g.scodoc_dept_id:
abort(404, "groupes inexistants dans ce département")
require_module = sco_preferences.get_preference(
"abs_require_module", formsemestre_id
)
# Vérfication du forçage du module
require_module = sco_preferences.get_preference("forcer_module", formsemestre_id)
# Récupération des étudiants du/des groupe(s)
etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
@ -810,7 +804,6 @@ def visu_assiduites_group():
moduleimpl_id = None
# --- Génération de l'HTML ---
sem = formsemestre.to_dict()
if groups_infos.tous_les_etuds_du_sem:
gr_tit = "en"
@ -841,6 +834,9 @@ def visu_assiduites_group():
],
)
# Récupération du semestre en dictionnaire
sem = formsemestre.to_dict()
return HTMLBuilder(
header,
_mini_timeline(),
@ -873,10 +869,14 @@ def visu_assiduites_group():
@permission_required(Permission.ScoView)
def etat_abs_date():
"""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_fin_str = request.args.get("date_fin")
title = request.args.get("desc")
group_ids: list[int] = request.args.get("group_ids", None)
# Vérification des dates
try:
date_debut = datetime.datetime.fromisoformat(date_debut_str)
except ValueError as exc:
@ -885,6 +885,8 @@ def etat_abs_date():
date_fin = datetime.datetime.fromisoformat(date_fin_str)
except ValueError as exc:
raise ScoValueError("date_fin invalide") from exc
# Vérification des groupes
if group_ids is None:
group_ids = []
else:
@ -893,25 +895,30 @@ def etat_abs_date():
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
# Récupération des étudiants des groupes
etuds = [
sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0]
for m in groups_infos.members
]
# Récupération des assiduites des étudiants
assiduites: Assiduite = Assiduite.query.filter(
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, Assiduite, date_debut, date_fin, False
)
# Génération d'objet étudiant simplifié (nom+lien cal, etat_assiduite)
etudiants: list[dict] = []
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()
etat = ""
if assi is not None and assi.etat != 0:
etat = scu.EtatAssiduite.inverse().get(assi.etat).name
# On génère l'objet simplifié
etudiant = {
"nom": f"""<a href="{url_for(
"assiduites.calendrier_etud",
@ -923,8 +930,10 @@ def etat_abs_date():
etudiants.append(etudiant)
# On tri les étudiants
etudiants = list(sorted(etudiants, key=lambda x: x["nom"]))
# Génération de l'HTML
header: str = html_sco_header.sco_header(
page_title=safehtml.html_to_safe_html(title),
init_qtip=True,
@ -948,6 +957,8 @@ def etat_abs_date():
@permission_required(Permission.ScoView)
def visu_assi_group():
"""Visualisation de l'assiduité d'un groupe entre deux dates"""
# Récupération des paramètres de la requête
dates = {
"debut": request.args.get("date_debut"),
"fin": request.args.get("date_fin"),
@ -961,13 +972,17 @@ def visu_assi_group():
group_ids = group_ids.split(",")
map(str, group_ids)
# Récupération des groupes, du semestre et des étudiants
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
formsemestre = db.session.get(FormSemestre, groups_infos.formsemestre_id)
etuds = etuds_sorted_from_ids([m["etudid"] for m in groups_infos.members])
# Génération du tableau des assiduités
table: TableAssi = TableAssi(
etuds=etuds, dates=list(dates.values()), formsemestre=formsemestre
)
# Export en XLS
if fmt.startswith("xls"):
return scu.send_file(
table.excel(),
@ -976,6 +991,7 @@ def visu_assi_group():
suffix=scu.XLSX_SUFFIX,
)
# récupération du/des noms du/des groupes
if groups_infos.tous_les_etuds_du_sem:
gr_tit = ""
grp = ""
@ -988,6 +1004,7 @@ def visu_assi_group():
grp + ' <span class="fontred">' + groups_infos.groups_titles + "</span>"
)
# Génération de la page
return render_template(
"assiduites/pages/visu_assi.j2",
assi_metric=scu.translate_assiduites_metric(
@ -1040,26 +1057,27 @@ def test():
@scodoc
@permission_required(Permission.AbsChange)
def signal_assiduites_diff():
# Récupération des paramètres de la requête
group_ids: list[int] = request.args.get("group_ids", None)
formsemestre_id: int = request.args.get("formsemestre_id", -1)
date: str = request.args.get("jour", datetime.date.today().isoformat())
date_deb: str = request.args.get("date_deb")
date_fin: str = request.args.get("date_fin")
semaine: str = request.args.get("semaine")
# Dans le cas où on donne une semaine plutot qu'un jour
if semaine is not None:
# On génère la semaine iso à partir de l'anne scolaire.
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(
semaine + "-1", "%Y-W%W-%w"
)
date_fin: datetime.date = date_deb + datetime.timedelta(days=6)
etudiants: list[dict] = []
titre = None
# Vérification du formsemestre_id
@ -1079,14 +1097,13 @@ def signal_assiduites_diff():
elif real_date > formsemestre.date_fin:
date = formsemestre.date_fin.isoformat()
# Vérification des groupes
if group_ids is None:
group_ids = []
else:
group_ids = group_ids.split(",")
map(str, group_ids)
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
if not groups_infos.members:
return (
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()
)
# Récupération des étudiants
etudiants.extend(
[
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"]))
# Génération de l'HTML
header: str = html_sco_header.sco_header(
page_title="Assiduité: saisie différée",
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
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)
if etud.dept_id != g.scodoc_dept_id:
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)
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):
# rediriger vers page saisie
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:
assiduite_unique: Assiduite = Assiduite.create_assiduite(
@ -1202,8 +1226,8 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
moduleimpl=evaluation.moduleimpl,
)
except ScoValueError as see:
# En cas d'erreur
msg: str = see.args[0]
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."
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.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")
# rediriger vers la page d'évaluation
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:
"""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(
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(
sco_preferences.get_preference(
"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(
etud.assiduites, Assiduite, semestre
).order_by(Assiduite.entry_date.desc())
@ -1252,10 +1281,17 @@ def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str:
etud.justificatifs, Justificatif, semestre
).order_by(Justificatif.entry_date.desc())
# On calcule les statistiques
stats: dict = scass.get_assiduites_stats(
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] = [
{"date": _get_date_str(assi.date_debut, assi.date_fin)}
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:
"""
_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():
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'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:
return "null"
try:
@ -1328,8 +1387,25 @@ def _get_days_between_dates(deb: str, fin: str):
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:
date = datetime.date.today().isoformat()
@ -1356,9 +1432,7 @@ def _differee(
)
def _module_selector(
formsemestre: FormSemestre, moduleimpl_id: int = None
) -> HTMLElement:
def _module_selector(formsemestre: FormSemestre, moduleimpl_id: int = None) -> str:
"""
_module_selector Génère un HTMLSelectElement à partir des moduleimpl du formsemestre
@ -1368,18 +1442,26 @@ def _module_selector(
Returns:
str: La représentation str d'un HTMLSelectElement
"""
# récupération des ues du semestre
ntc: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = ntc.get_ues_stat_dict()
modimpls_list: list[dict] = []
ues = ntc.get_ues_stat_dict()
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"])
# 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"
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:
modname: str = (
(modimpl["module"]["code"] or "")
@ -1388,11 +1470,6 @@ def _module_selector(
)
modules.append({"moduleimpl_id": modimpl["moduleimpl_id"], "name": modname})
try:
moduleimpl_id = int(moduleimpl_id)
except (ValueError, TypeError):
moduleimpl_id = None
return render_template(
"assiduites/widgets/moduleimpl_selector.j2",
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(
"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(
"assiduites/widgets/timeline.j2",
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(
"assiduites/widgets/minitimeline.j2",
t_start=ScoDocSiteConfig.assi_get_rounded_time("assi_morning_time", "08:00:00"),

View File

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

View File

@ -26,6 +26,7 @@ def downgrade_module(
dept_etudid: list[int] = None
dept_id: int = None
# Récupération du département si spécifié
if dept is not None:
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_id = departement.id
# Suppression des assiduités
if assiduites:
_remove_assiduites(dept_etudid)
# Suppression des justificatifs
if justificatifs:
_remove_justificatifs(dept_etudid)
_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 assiduites:
db.session.execute(
@ -51,26 +55,52 @@ def downgrade_module(
sa.text("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1")
)
# On valide l'opération sur la bdd
db.session.commit()
# On affiche un message pour l'utilisateur
print(
f"{TerminalColor.GREEN}Le module assiduité a bien été remis à zero.{TerminalColor.RESET}"
)
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:
# Si pas d'étudids alors on supprime toutes les assiduités
Assiduite.query.delete()
else:
# Sinon on supprime que les assiduités des étudiants donnés
Assiduite.query.filter(Assiduite.etudid.in_(dept_etudid)).delete()
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:
# Si pas d'étudids alors on supprime tous les justificatifs
Justificatif.query.delete()
else:
# Sinon on supprime que les justificatifs des étudiants donnés
Justificatif.query.filter(Justificatif.etudid.in_(dept_etudid)).delete()
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)