2023-04-17 15:34:00 +02:00
|
|
|
# -*- coding: UTF-8 -*
|
|
|
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
|
|
|
"""
|
|
|
|
from datetime import datetime
|
2023-12-22 15:22:57 +01:00
|
|
|
from flask_login import current_user
|
2023-12-06 02:29:06 +01:00
|
|
|
from flask_sqlalchemy.query import Query
|
2024-01-12 10:00:53 +01:00
|
|
|
from sqlalchemy.exc import DataError
|
2023-12-06 02:29:06 +01:00
|
|
|
|
|
|
|
from app import db, log, g, set_sco_dept
|
2023-12-22 15:22:57 +01:00
|
|
|
from app.models import (
|
|
|
|
ModuleImpl,
|
|
|
|
Module,
|
|
|
|
Scolog,
|
|
|
|
FormSemestre,
|
|
|
|
FormSemestreInscription,
|
|
|
|
ScoDocModel,
|
|
|
|
)
|
2023-04-17 15:34:00 +02:00
|
|
|
from app.models.etudiants import Identite
|
2023-06-30 17:24:16 +02:00
|
|
|
from app.auth.models import User
|
2023-10-09 23:01:19 +02:00
|
|
|
from app.scodoc import sco_abs_notification
|
2023-12-22 15:22:57 +01:00
|
|
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
2023-04-17 15:34:00 +02:00
|
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
2023-12-22 15:22:57 +01:00
|
|
|
from app.scodoc.sco_permissions import Permission
|
2023-04-17 15:34:00 +02:00
|
|
|
from app.scodoc.sco_utils import (
|
|
|
|
EtatAssiduite,
|
|
|
|
EtatJustificatif,
|
|
|
|
localize_datetime,
|
2023-10-26 15:52:53 +02:00
|
|
|
is_assiduites_module_forced,
|
2024-01-05 10:06:16 +01:00
|
|
|
NonWorkDays,
|
2023-04-17 15:34:00 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2023-12-22 15:22:57 +01:00
|
|
|
class Assiduite(ScoDocModel):
|
2023-04-17 15:34:00 +02:00
|
|
|
"""
|
|
|
|
Représente une assiduité:
|
|
|
|
- une plage horaire lié à un état et un étudiant
|
|
|
|
- un module si spécifiée
|
|
|
|
- une description si spécifiée
|
|
|
|
"""
|
|
|
|
|
|
|
|
__tablename__ = "assiduites"
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True, nullable=False)
|
|
|
|
assiduite_id = db.synonym("id")
|
|
|
|
|
|
|
|
date_debut = db.Column(
|
|
|
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
|
|
|
)
|
|
|
|
date_fin = db.Column(
|
|
|
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
|
|
|
)
|
|
|
|
|
|
|
|
moduleimpl_id = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("notes_moduleimpl.id", ondelete="SET NULL"),
|
|
|
|
)
|
|
|
|
etudid = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
|
|
index=True,
|
|
|
|
nullable=False,
|
|
|
|
)
|
|
|
|
etat = db.Column(db.Integer, nullable=False)
|
|
|
|
|
2023-07-20 13:10:26 +02:00
|
|
|
description = db.Column(db.Text)
|
2023-04-17 15:34:00 +02:00
|
|
|
|
|
|
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
|
|
|
|
|
|
|
user_id = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
|
|
|
nullable=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
|
|
|
|
2023-07-31 09:41:32 +02:00
|
|
|
external_data = db.Column(db.JSON, nullable=True)
|
|
|
|
|
2023-07-29 18:32:29 +02:00
|
|
|
# Déclare la relation "joined" car on va très souvent vouloir récupérer
|
|
|
|
# l'étudiant en même tant que l'assiduité (perf.: évite nouvelle requete SQL)
|
|
|
|
etudiant = db.relationship("Identite", back_populates="assiduites", lazy="joined")
|
2023-10-06 16:32:06 +02:00
|
|
|
# En revanche, user est rarement accédé:
|
|
|
|
user = db.relationship(
|
|
|
|
"User",
|
|
|
|
backref=db.backref(
|
|
|
|
"assiduites", lazy="select", order_by="Assiduite.entry_date"
|
|
|
|
),
|
|
|
|
lazy="select",
|
|
|
|
)
|
2023-07-29 18:32:29 +02:00
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
def to_dict(self, format_api=True) -> dict:
|
|
|
|
"""Retourne la représentation json de l'assiduité"""
|
|
|
|
etat = self.etat
|
2023-12-06 02:29:06 +01:00
|
|
|
user: User | None = None
|
2023-04-17 15:34:00 +02:00
|
|
|
if format_api:
|
2023-10-27 16:05:40 +02:00
|
|
|
# format api utilise les noms "present,absent,retard" au lieu des int
|
2023-04-17 15:34:00 +02:00
|
|
|
etat = EtatAssiduite.inverse().get(self.etat).name
|
2023-06-30 17:24:16 +02:00
|
|
|
if self.user_id is not None:
|
2023-09-27 23:02:32 +02:00
|
|
|
user = db.session.get(User, self.user_id)
|
2023-04-17 15:34:00 +02:00
|
|
|
data = {
|
|
|
|
"assiduite_id": self.id,
|
|
|
|
"etudid": self.etudid,
|
2023-07-29 19:04:35 +02:00
|
|
|
"code_nip": self.etudiant.code_nip,
|
2023-04-17 15:34:00 +02:00
|
|
|
"moduleimpl_id": self.moduleimpl_id,
|
|
|
|
"date_debut": self.date_debut,
|
|
|
|
"date_fin": self.date_fin,
|
|
|
|
"etat": etat,
|
2023-07-20 13:10:26 +02:00
|
|
|
"desc": self.description,
|
2023-04-17 15:34:00 +02:00
|
|
|
"entry_date": self.entry_date,
|
2023-09-27 23:02:32 +02:00
|
|
|
"user_id": None if user is None else user.id, # l'uid
|
|
|
|
"user_name": None if user is None else user.user_name, # le login
|
2023-10-06 16:32:06 +02:00
|
|
|
"user_nom_complet": None
|
|
|
|
if user is None
|
|
|
|
else user.get_nomcomplet(), # "Marie Dupont"
|
2023-04-17 15:34:00 +02:00
|
|
|
"est_just": self.est_just,
|
2023-07-31 09:41:32 +02:00
|
|
|
"external_data": self.external_data,
|
2023-04-17 15:34:00 +02:00
|
|
|
}
|
|
|
|
return data
|
|
|
|
|
2023-07-26 13:27:57 +02:00
|
|
|
def __str__(self) -> str:
|
|
|
|
"chaine pour journaux et debug (lisible par humain français)"
|
|
|
|
try:
|
|
|
|
etat_str = EtatAssiduite(self.etat).name.lower().capitalize()
|
|
|
|
except ValueError:
|
|
|
|
etat_str = "Invalide"
|
|
|
|
return f"""{etat_str} {
|
|
|
|
"just." if self.est_just else "non just."
|
|
|
|
} de {
|
|
|
|
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
|
|
|
} à {
|
|
|
|
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
|
|
|
}"""
|
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
@classmethod
|
|
|
|
def create_assiduite(
|
|
|
|
cls,
|
|
|
|
etud: Identite,
|
|
|
|
date_debut: datetime,
|
|
|
|
date_fin: datetime,
|
|
|
|
etat: EtatAssiduite,
|
|
|
|
moduleimpl: ModuleImpl = None,
|
|
|
|
description: str = None,
|
|
|
|
entry_date: datetime = None,
|
|
|
|
user_id: int = None,
|
|
|
|
est_just: bool = False,
|
2023-07-31 09:41:32 +02:00
|
|
|
external_data: dict = None,
|
2023-10-09 23:01:19 +02:00
|
|
|
notify_mail=False,
|
2023-09-12 19:57:39 +02:00
|
|
|
) -> "Assiduite":
|
2023-12-06 02:04:10 +01:00
|
|
|
"""Créer une nouvelle assiduité pour l'étudiant.
|
2023-12-22 15:22:57 +01:00
|
|
|
Les datetime doivent être en timezone serveur.
|
2023-12-06 02:04:10 +01:00
|
|
|
Raises ScoValueError en cas de conflit ou erreur.
|
|
|
|
"""
|
2023-10-11 14:45:06 +02:00
|
|
|
if date_debut.tzinfo is None:
|
|
|
|
log(
|
|
|
|
f"Warning: create_assiduite: date_debut without timezone ({date_debut})"
|
|
|
|
)
|
|
|
|
if date_fin.tzinfo is None:
|
|
|
|
log(f"Warning: create_assiduite: date_fin without timezone ({date_fin})")
|
2024-01-05 10:06:16 +01:00
|
|
|
|
|
|
|
# Vérification jours non travaillés
|
|
|
|
# -> vérifie si la date de début ou la date de fin est sur un jour non travaillé
|
|
|
|
# On récupère les formsemestres des dates de début et de fin
|
2024-01-05 23:20:32 +01:00
|
|
|
formsemestre_date_debut: FormSemestre = get_formsemestre_from_data(
|
2024-01-05 10:06:16 +01:00
|
|
|
{
|
|
|
|
"etudid": etud.id,
|
|
|
|
"date_debut": date_debut,
|
|
|
|
"date_fin": date_debut,
|
|
|
|
}
|
|
|
|
)
|
2024-01-05 23:20:32 +01:00
|
|
|
formsemestre_date_fin: FormSemestre = get_formsemestre_from_data(
|
2024-01-05 10:06:16 +01:00
|
|
|
{
|
|
|
|
"etudid": etud.id,
|
|
|
|
"date_debut": date_fin,
|
|
|
|
"date_fin": date_fin,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
if date_debut.weekday() in NonWorkDays.get_all_non_work_days(
|
2024-01-05 23:20:32 +01:00
|
|
|
formsemestre_id=formsemestre_date_debut
|
2024-01-05 10:06:16 +01:00
|
|
|
):
|
|
|
|
raise ScoValueError("La date de début n'est pas un jour travaillé")
|
|
|
|
if date_fin.weekday() in NonWorkDays.get_all_non_work_days(
|
2024-01-05 23:20:32 +01:00
|
|
|
formsemestre_id=formsemestre_date_fin
|
2024-01-05 10:06:16 +01:00
|
|
|
):
|
|
|
|
raise ScoValueError("La date de fin n'est pas un jour travaillé")
|
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
# Vérification de non duplication des périodes
|
2023-08-14 01:08:04 +02:00
|
|
|
assiduites: Query = etud.assiduites
|
2023-04-17 15:34:00 +02:00
|
|
|
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
2023-12-14 20:50:27 +01:00
|
|
|
log(
|
2024-01-05 23:20:32 +01:00
|
|
|
f"""create_assiduite: period_conflicting etudid={etud.id} date_debut={
|
|
|
|
date_debut} date_fin={date_fin}"""
|
2023-12-14 20:50:27 +01:00
|
|
|
)
|
2023-04-17 15:34:00 +02:00
|
|
|
raise ScoValueError(
|
2023-10-09 23:01:19 +02:00
|
|
|
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
2023-04-17 15:34:00 +02:00
|
|
|
)
|
2023-08-09 09:57:47 +02:00
|
|
|
|
|
|
|
if not est_just:
|
|
|
|
est_just = (
|
2023-09-11 09:05:29 +02:00
|
|
|
len(
|
|
|
|
get_justifs_from_date(etud.etudid, date_debut, date_fin, valid=True)
|
|
|
|
)
|
|
|
|
> 0
|
2023-08-09 09:57:47 +02:00
|
|
|
)
|
|
|
|
|
2023-10-09 23:01:19 +02:00
|
|
|
moduleimpl_id = None
|
2023-04-17 15:34:00 +02:00
|
|
|
if moduleimpl is not None:
|
2023-10-09 23:01:19 +02:00
|
|
|
# Vérification de l'inscription de l'étudiant
|
2023-04-17 15:34:00 +02:00
|
|
|
if moduleimpl.est_inscrit(etud):
|
2023-10-09 23:01:19 +02:00
|
|
|
moduleimpl_id = moduleimpl.id
|
2023-04-17 15:34:00 +02:00
|
|
|
else:
|
2023-09-20 12:45:24 +02:00
|
|
|
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
2023-10-26 15:52:53 +02:00
|
|
|
elif not (
|
|
|
|
external_data is not None and external_data.get("module") is not None
|
|
|
|
):
|
|
|
|
# Vérification si module forcé
|
|
|
|
formsemestre: FormSemestre = get_formsemestre_from_data(
|
|
|
|
{"etudid": etud.id, "date_debut": date_debut, "date_fin": date_fin}
|
|
|
|
)
|
|
|
|
force: bool
|
|
|
|
|
|
|
|
if formsemestre:
|
|
|
|
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
|
|
|
else:
|
|
|
|
force = is_assiduites_module_forced(dept_id=etud.dept_id)
|
|
|
|
|
|
|
|
if force:
|
|
|
|
raise ScoValueError("Module non renseigné")
|
|
|
|
|
2023-10-09 23:01:19 +02:00
|
|
|
nouv_assiduite = Assiduite(
|
|
|
|
date_debut=date_debut,
|
|
|
|
date_fin=date_fin,
|
|
|
|
description=description,
|
|
|
|
entry_date=entry_date,
|
|
|
|
est_just=est_just,
|
|
|
|
etat=etat,
|
|
|
|
etudiant=etud,
|
|
|
|
external_data=external_data,
|
|
|
|
moduleimpl_id=moduleimpl_id,
|
|
|
|
user_id=user_id,
|
|
|
|
)
|
2023-07-31 09:41:32 +02:00
|
|
|
db.session.add(nouv_assiduite)
|
2023-12-05 21:04:38 +01:00
|
|
|
db.session.flush()
|
|
|
|
log(f"create_assiduite: {etud.id} id={nouv_assiduite.id} {nouv_assiduite}")
|
2023-07-26 13:27:57 +02:00
|
|
|
Scolog.logdb(
|
|
|
|
method="create_assiduite",
|
|
|
|
etudid=etud.id,
|
|
|
|
msg=f"assiduité: {nouv_assiduite}",
|
2023-04-17 15:34:00 +02:00
|
|
|
)
|
2023-10-09 23:01:19 +02:00
|
|
|
if notify_mail and etat == EtatAssiduite.ABSENT:
|
|
|
|
sco_abs_notification.abs_notify(etud.id, nouv_assiduite.date_debut)
|
2023-04-17 15:34:00 +02:00
|
|
|
return nouv_assiduite
|
|
|
|
|
2024-01-12 10:00:53 +01:00
|
|
|
def set_moduleimpl(self, moduleimpl_id: int | str):
|
|
|
|
"""Mise à jour du moduleimpl_id
|
|
|
|
Les valeurs du champs "moduleimpl_id" possibles sont :
|
|
|
|
- <int> (un id classique)
|
|
|
|
- <str> ("autre" ou "<id>")
|
|
|
|
- None (pas de moduleimpl_id)
|
|
|
|
Si la valeur est "autre" il faut:
|
|
|
|
- mettre à None assiduité.moduleimpl_id
|
|
|
|
- mettre à jour assiduite.external_data["module"] = "autre"
|
|
|
|
En fonction de la configuration du semestre la valeur `None` peut-être considérée comme invalide.
|
|
|
|
- Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité
|
|
|
|
"""
|
|
|
|
moduleimpl: ModuleImpl = None
|
2023-12-13 00:00:49 +01:00
|
|
|
try:
|
2024-01-12 10:00:53 +01:00
|
|
|
# ne lève une erreur que si moduleimpl_id est une chaine de caractère non parsable (parseInt)
|
|
|
|
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
|
|
|
# moduleImpl est soit :
|
|
|
|
# - None si moduleimpl_id==None
|
|
|
|
# - None si moduleimpl_id==<int> non reconnu
|
|
|
|
# - ModuleImpl si <int|str> valide
|
|
|
|
|
|
|
|
# Vérification ModuleImpl not None (raise ScoValueError)
|
|
|
|
if moduleimpl is None and self._check_force_module(moduleimpl):
|
|
|
|
# Ici uniquement si on est autorisé à ne pas avoir de module
|
|
|
|
self.moduleimpl_id = None
|
|
|
|
return
|
|
|
|
|
|
|
|
# Vérification Inscription ModuleImpl (raise ScoValueError)
|
2023-11-24 18:07:30 +01:00
|
|
|
if moduleimpl.est_inscrit(self.etudiant):
|
|
|
|
self.moduleimpl_id = moduleimpl.id
|
|
|
|
else:
|
|
|
|
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
2024-01-12 10:00:53 +01:00
|
|
|
|
|
|
|
except DataError:
|
|
|
|
# On arrive ici si moduleimpl_id == "autre" ou moduleimpl_id == <str> non parsé
|
|
|
|
|
|
|
|
if moduleimpl_id != "autre":
|
|
|
|
raise ScoValueError("Module non reconnu")
|
|
|
|
|
|
|
|
# Configuration de external_data pour Module Autre
|
|
|
|
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
|
|
|
|
# Sinon on met à jour external_data["module"] à "autre"
|
|
|
|
|
2023-11-24 18:07:30 +01:00
|
|
|
if self.external_data is None:
|
2024-01-12 10:00:53 +01:00
|
|
|
self.external_data = {"module": "autre"}
|
2023-11-24 18:07:30 +01:00
|
|
|
else:
|
2024-01-12 10:00:53 +01:00
|
|
|
self.external_data["module"] = "autre"
|
2023-11-24 18:07:30 +01:00
|
|
|
|
2024-01-12 10:00:53 +01:00
|
|
|
# Dans tous les cas une fois fait, assiduite.moduleimpl_id doit être None
|
|
|
|
self.moduleimpl_id = None
|
2023-11-24 18:07:30 +01:00
|
|
|
|
2024-01-12 10:00:53 +01:00
|
|
|
# Ici pas de vérification du force module car on l'a mis dans "external_data"
|
2023-11-24 18:07:30 +01:00
|
|
|
|
2023-12-07 12:38:47 +01:00
|
|
|
def supprime(self):
|
|
|
|
"Supprime l'assiduité. Log et commit."
|
2023-11-24 13:58:03 +01:00
|
|
|
from app.scodoc import sco_assiduites as scass
|
|
|
|
|
|
|
|
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
|
|
|
# route sans département
|
|
|
|
set_sco_dept(self.etudiant.departement.acronym)
|
|
|
|
obj_dict: dict = self.to_dict()
|
|
|
|
# Suppression de l'objet et LOG
|
|
|
|
log(f"delete_assidutite: {self.etudiant.id} {self}")
|
|
|
|
Scolog.logdb(
|
2023-12-06 02:29:06 +01:00
|
|
|
method="delete_assiduite",
|
2023-11-24 13:58:03 +01:00
|
|
|
etudid=self.etudiant.id,
|
|
|
|
msg=f"Assiduité: {self}",
|
|
|
|
)
|
|
|
|
db.session.delete(self)
|
2023-12-06 02:29:06 +01:00
|
|
|
db.session.commit()
|
2023-11-24 13:58:03 +01:00
|
|
|
# Invalidation du cache
|
|
|
|
scass.simple_invalidate_cache(obj_dict)
|
|
|
|
|
|
|
|
def get_formsemestre(self) -> FormSemestre:
|
2023-12-07 12:38:47 +01:00
|
|
|
"""Le formsemestre associé.
|
|
|
|
Attention: en cas d'inscription multiple prend arbitrairement l'un des semestres.
|
|
|
|
A utiliser avec précaution !
|
|
|
|
"""
|
2023-11-24 13:58:03 +01:00
|
|
|
return get_formsemestre_from_data(self.to_dict())
|
|
|
|
|
|
|
|
def get_module(self, traduire: bool = False) -> int | str:
|
2023-12-07 12:38:47 +01:00
|
|
|
"TODO"
|
2023-11-24 13:58:03 +01:00
|
|
|
if self.moduleimpl_id is not None:
|
|
|
|
if traduire:
|
|
|
|
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
|
|
|
mod: Module = Module.query.get(modimpl.module_id)
|
|
|
|
return f"{mod.code} {mod.titre}"
|
|
|
|
|
|
|
|
elif self.external_data is not None and "module" in self.external_data:
|
|
|
|
return (
|
|
|
|
"Tout module"
|
|
|
|
if self.external_data["module"] == "Autre"
|
|
|
|
else self.external_data["module"]
|
|
|
|
)
|
|
|
|
|
|
|
|
return "Non spécifié" if traduire else None
|
|
|
|
|
2024-01-11 17:19:56 +01:00
|
|
|
def get_saisie(self) -> str:
|
|
|
|
"""
|
|
|
|
retourne le texte "saisie le <date> par <User>"
|
|
|
|
"""
|
|
|
|
|
|
|
|
date: str = self.entry_date.strftime("%d/%m/%Y à %H:%M")
|
|
|
|
utilisateur: str = ""
|
|
|
|
if self.user != None:
|
|
|
|
self.user: User
|
|
|
|
utilisateur = f"par {self.user.get_prenomnom()}"
|
|
|
|
|
|
|
|
return f"saisie le {date} {utilisateur}"
|
|
|
|
|
2024-01-12 10:00:53 +01:00
|
|
|
def _check_force_module(self, moduleimpl: ModuleImpl) -> bool:
|
|
|
|
# Vérification si module forcé
|
|
|
|
formsemestre: FormSemestre = get_formsemestre_from_data(
|
|
|
|
{
|
|
|
|
"etudid": self.etudid,
|
|
|
|
"date_debut": self.date_debut,
|
|
|
|
"date_fin": self.date_fin,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
force: bool
|
|
|
|
|
|
|
|
if formsemestre:
|
|
|
|
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
|
|
|
else:
|
|
|
|
force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id)
|
|
|
|
|
|
|
|
if force:
|
|
|
|
raise ScoValueError("Module non renseigné")
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
|
2023-12-22 15:22:57 +01:00
|
|
|
class Justificatif(ScoDocModel):
|
2023-04-17 15:34:00 +02:00
|
|
|
"""
|
|
|
|
Représente un justificatif:
|
|
|
|
- une plage horaire lié à un état et un étudiant
|
|
|
|
- une raison si spécifiée
|
|
|
|
- un fichier si spécifié
|
|
|
|
"""
|
|
|
|
|
|
|
|
__tablename__ = "justificatifs"
|
|
|
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
|
|
justif_id = db.synonym("id")
|
|
|
|
|
|
|
|
date_debut = db.Column(
|
|
|
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
|
|
|
)
|
|
|
|
date_fin = db.Column(
|
|
|
|
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
|
|
|
)
|
|
|
|
|
|
|
|
etudid = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
|
|
|
index=True,
|
|
|
|
nullable=False,
|
|
|
|
)
|
|
|
|
etat = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
nullable=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
2023-12-05 21:04:38 +01:00
|
|
|
"date de création de l'élément: date de saisie"
|
|
|
|
# pourrait devenir date de dépot au secrétariat, si différente
|
2023-04-17 15:34:00 +02:00
|
|
|
|
|
|
|
user_id = db.Column(
|
|
|
|
db.Integer,
|
|
|
|
db.ForeignKey("user.id", ondelete="SET NULL"),
|
|
|
|
nullable=True,
|
|
|
|
index=True,
|
|
|
|
)
|
|
|
|
|
|
|
|
raison = db.Column(db.Text())
|
|
|
|
|
|
|
|
# Archive_id -> sco_archives_justificatifs.py
|
|
|
|
fichier = db.Column(db.Text())
|
|
|
|
|
2023-07-31 09:41:32 +02:00
|
|
|
# Déclare la relation "joined" car on va très souvent vouloir récupérer
|
|
|
|
# l'étudiant en même tant que le justificatif (perf.: évite nouvelle requete SQL)
|
|
|
|
etudiant = db.relationship(
|
|
|
|
"Identite", back_populates="justificatifs", lazy="joined"
|
|
|
|
)
|
|
|
|
|
|
|
|
external_data = db.Column(db.JSON, nullable=True)
|
2023-07-29 18:32:29 +02:00
|
|
|
|
2023-12-22 15:22:57 +01:00
|
|
|
@classmethod
|
|
|
|
def get_justificatif(cls, justif_id: int) -> "Justificatif":
|
|
|
|
"""Justificatif ou 404, cherche uniquement dans le département courant"""
|
|
|
|
query = Justificatif.query.filter_by(id=justif_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
return query.first_or_404()
|
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
def to_dict(self, format_api: bool = False) -> dict:
|
|
|
|
"""transformation de l'objet en dictionnaire sérialisable"""
|
|
|
|
|
|
|
|
etat = self.etat
|
2023-06-30 17:24:16 +02:00
|
|
|
username = self.user_id
|
2023-04-17 15:34:00 +02:00
|
|
|
|
|
|
|
if format_api:
|
|
|
|
etat = EtatJustificatif.inverse().get(self.etat).name
|
2023-06-30 17:24:16 +02:00
|
|
|
if self.user_id is not None:
|
2023-07-11 11:35:50 +02:00
|
|
|
user: User = db.session.get(User, self.user_id)
|
2023-06-30 17:24:16 +02:00
|
|
|
if user is None:
|
|
|
|
username = "Non renseigné"
|
|
|
|
else:
|
|
|
|
username = user.get_prenomnom()
|
2023-04-17 15:34:00 +02:00
|
|
|
|
|
|
|
data = {
|
|
|
|
"justif_id": self.justif_id,
|
|
|
|
"etudid": self.etudid,
|
2023-07-31 09:41:32 +02:00
|
|
|
"code_nip": self.etudiant.code_nip,
|
2023-04-17 15:34:00 +02:00
|
|
|
"date_debut": self.date_debut,
|
|
|
|
"date_fin": self.date_fin,
|
|
|
|
"etat": etat,
|
|
|
|
"raison": self.raison,
|
|
|
|
"fichier": self.fichier,
|
|
|
|
"entry_date": self.entry_date,
|
2023-06-30 17:24:16 +02:00
|
|
|
"user_id": username,
|
2023-07-31 09:41:32 +02:00
|
|
|
"external_data": self.external_data,
|
2023-04-17 15:34:00 +02:00
|
|
|
}
|
|
|
|
return data
|
|
|
|
|
2023-12-12 03:05:31 +01:00
|
|
|
def __repr__(self) -> str:
|
2023-07-26 16:00:23 +02:00
|
|
|
"chaine pour journaux et debug (lisible par humain français)"
|
|
|
|
try:
|
|
|
|
etat_str = EtatJustificatif(self.etat).name
|
|
|
|
except ValueError:
|
|
|
|
etat_str = "Invalide"
|
2023-12-12 03:05:31 +01:00
|
|
|
return f"""Justificatif id={self.id} {etat_str} de {
|
2023-07-26 16:00:23 +02:00
|
|
|
self.date_debut.strftime("%d/%m/%Y %Hh%M")
|
|
|
|
} à {
|
|
|
|
self.date_fin.strftime("%d/%m/%Y %Hh%M")
|
|
|
|
}"""
|
|
|
|
|
2023-12-22 15:22:57 +01:00
|
|
|
@classmethod
|
|
|
|
def convert_dict_fields(cls, args: dict) -> dict:
|
|
|
|
"""Convert fields. Called by ScoDocModel's create_from_dict, edit and from_dict
|
|
|
|
Raises ScoValueError si paramètres incorrects.
|
|
|
|
"""
|
|
|
|
if not isinstance(args["date_debut"], datetime) or not isinstance(
|
|
|
|
args["date_fin"], datetime
|
|
|
|
):
|
|
|
|
raise ScoValueError("type date incorrect")
|
|
|
|
if args["date_fin"] <= args["date_debut"]:
|
|
|
|
raise ScoValueError("dates incompatibles")
|
|
|
|
if args["entry_date"] and not isinstance(args["entry_date"], datetime):
|
|
|
|
raise ScoValueError("type entry_date incorrect")
|
|
|
|
return args
|
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
@classmethod
|
|
|
|
def create_justificatif(
|
|
|
|
cls,
|
2023-12-22 15:22:57 +01:00
|
|
|
etudiant: Identite,
|
2023-04-17 15:34:00 +02:00
|
|
|
date_debut: datetime,
|
|
|
|
date_fin: datetime,
|
|
|
|
etat: EtatJustificatif,
|
|
|
|
raison: str = None,
|
|
|
|
entry_date: datetime = None,
|
|
|
|
user_id: int = None,
|
2023-07-31 09:41:32 +02:00
|
|
|
external_data: dict = None,
|
2023-09-12 19:57:39 +02:00
|
|
|
) -> "Justificatif":
|
2023-12-22 15:22:57 +01:00
|
|
|
"""Créer un nouveau justificatif pour l'étudiant.
|
|
|
|
Raises ScoValueError si paramètres incorrects.
|
|
|
|
"""
|
|
|
|
nouv_justificatif = cls.create_from_dict(locals())
|
2023-12-16 22:53:02 +01:00
|
|
|
db.session.commit()
|
2023-12-22 15:22:57 +01:00
|
|
|
log(f"create_justificatif: etudid={etudiant.id} {nouv_justificatif}")
|
2023-07-26 16:00:23 +02:00
|
|
|
Scolog.logdb(
|
|
|
|
method="create_justificatif",
|
2023-12-22 15:22:57 +01:00
|
|
|
etudid=etudiant.id,
|
2023-07-26 16:00:23 +02:00
|
|
|
msg=f"justificatif: {nouv_justificatif}",
|
|
|
|
)
|
2023-04-17 15:34:00 +02:00
|
|
|
return nouv_justificatif
|
|
|
|
|
2023-12-07 12:38:47 +01:00
|
|
|
def supprime(self):
|
|
|
|
"Supprime le justificatif. Log et commit."
|
2023-11-24 13:58:03 +01:00
|
|
|
from app.scodoc import sco_assiduites as scass
|
2023-12-07 14:49:03 +01:00
|
|
|
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
2023-11-24 13:58:03 +01:00
|
|
|
|
|
|
|
# Récupération de l'archive du justificatif
|
|
|
|
archive_name: str = self.fichier
|
|
|
|
|
|
|
|
if archive_name is not None:
|
|
|
|
# Si elle existe : on essaye de la supprimer
|
|
|
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
|
|
|
try:
|
|
|
|
archiver.delete_justificatif(self.etudiant, archive_name)
|
|
|
|
except ValueError:
|
|
|
|
pass
|
|
|
|
if g.scodoc_dept is None and self.etudiant.dept_id is not None:
|
|
|
|
# route sans département
|
|
|
|
set_sco_dept(self.etudiant.departement.acronym)
|
|
|
|
# On invalide le cache
|
|
|
|
scass.simple_invalidate_cache(self.to_dict())
|
|
|
|
# Suppression de l'objet et LOG
|
|
|
|
log(f"delete_justificatif: {self.etudiant.id} {self}")
|
|
|
|
Scolog.logdb(
|
2023-12-07 12:38:47 +01:00
|
|
|
method="delete_justificatif",
|
2023-11-24 13:58:03 +01:00
|
|
|
etudid=self.etudiant.id,
|
|
|
|
msg=f"Justificatif: {self}",
|
|
|
|
)
|
|
|
|
db.session.delete(self)
|
2023-12-06 02:29:06 +01:00
|
|
|
db.session.commit()
|
2023-11-24 13:58:03 +01:00
|
|
|
# On actualise les assiduités justifiées de l'étudiant concerné
|
|
|
|
compute_assiduites_justified(
|
|
|
|
self.etudid,
|
|
|
|
Justificatif.query.filter_by(etudid=self.etudid).all(),
|
|
|
|
True,
|
|
|
|
)
|
|
|
|
|
2023-12-22 15:22:57 +01:00
|
|
|
def get_fichiers(self) -> tuple[list[str], int]:
|
|
|
|
"""Renvoie la liste des noms de fichiers justicatifs
|
|
|
|
accessibles par l'utilisateur courant et le nombre total
|
|
|
|
de fichiers.
|
|
|
|
(ces fichiers sont dans l'archive associée)
|
|
|
|
"""
|
|
|
|
if self.fichier is None:
|
|
|
|
return [], 0
|
|
|
|
archive_name: str = self.fichier
|
|
|
|
archiver: JustificatifArchiver = JustificatifArchiver()
|
|
|
|
filenames = archiver.list_justificatifs(archive_name, self.etudiant)
|
|
|
|
accessible_filenames = []
|
2023-12-23 13:53:02 +01:00
|
|
|
#
|
2023-12-22 15:22:57 +01:00
|
|
|
for filename in filenames:
|
|
|
|
if int(filename[1]) == current_user.id or current_user.has_permission(
|
|
|
|
Permission.AbsJustifView
|
|
|
|
):
|
|
|
|
accessible_filenames.append(filename[0])
|
|
|
|
return accessible_filenames, len(filenames)
|
|
|
|
|
2023-04-17 15:34:00 +02:00
|
|
|
|
|
|
|
def is_period_conflicting(
|
|
|
|
date_debut: datetime,
|
|
|
|
date_fin: datetime,
|
2023-08-14 01:08:04 +02:00
|
|
|
collection: Query,
|
2023-09-12 19:57:39 +02:00
|
|
|
collection_cls: Assiduite | Justificatif,
|
2023-04-17 15:34:00 +02:00
|
|
|
) -> bool:
|
|
|
|
"""
|
|
|
|
Vérifie si une date n'entre pas en collision
|
|
|
|
avec les justificatifs ou assiduites déjà présentes
|
|
|
|
"""
|
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
# On s'assure que les dates soient avec TimeZone
|
2023-04-17 15:34:00 +02:00
|
|
|
date_debut = localize_datetime(date_debut)
|
|
|
|
date_fin = localize_datetime(date_fin)
|
|
|
|
|
|
|
|
count: int = collection.filter(
|
|
|
|
collection_cls.date_debut < date_fin, collection_cls.date_fin > date_debut
|
|
|
|
).count()
|
|
|
|
|
|
|
|
return count > 0
|
|
|
|
|
|
|
|
|
|
|
|
def compute_assiduites_justified(
|
2023-07-30 16:34:05 +02:00
|
|
|
etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False
|
2023-04-17 15:34:00 +02:00
|
|
|
) -> list[int]:
|
2023-07-30 16:34:05 +02:00
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
etudid (int): l'identifiant de l'étudiant
|
|
|
|
justificatifs (list[Justificatif]): La liste des justificatifs qui seront utilisés
|
|
|
|
reset (bool, optional): remet à false les assiduites non justifiés. Defaults to False.
|
2023-04-17 15:34:00 +02:00
|
|
|
|
2023-07-30 16:34:05 +02:00
|
|
|
Returns:
|
|
|
|
list[int]: la liste des assiduités qui ont été justifiées.
|
2023-04-17 15:34:00 +02:00
|
|
|
"""
|
2024-01-11 17:24:01 +01:00
|
|
|
# TODO à optimiser (car très long avec 40000 assiduités)
|
2023-10-27 16:05:40 +02:00
|
|
|
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
|
2023-07-30 16:34:05 +02:00
|
|
|
if justificatifs is None:
|
2023-10-27 16:05:40 +02:00
|
|
|
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
|
|
|
etudid=etudid
|
|
|
|
).all()
|
2023-04-17 15:34:00 +02:00
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
# On ne prend que les justificatifs valides
|
2023-09-05 14:25:38 +02:00
|
|
|
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
|
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
# On récupère les assiduités de l'étudiant
|
2023-07-30 16:34:05 +02:00
|
|
|
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
|
2023-04-17 15:34:00 +02:00
|
|
|
|
2023-07-30 16:34:05 +02:00
|
|
|
assiduites_justifiees: list[int] = []
|
|
|
|
|
|
|
|
for assi in assiduites:
|
2023-10-27 16:05:40 +02:00
|
|
|
# On ne justifie pas les Présences
|
2023-09-05 14:25:38 +02:00
|
|
|
if assi.etat == EtatAssiduite.PRESENT:
|
|
|
|
continue
|
2023-09-24 09:18:13 +02:00
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
# On récupère les justificatifs qui justifient l'assiduité `assi`
|
2023-09-24 09:18:13 +02:00
|
|
|
assi_justificatifs = Justificatif.query.filter(
|
|
|
|
Justificatif.etudid == assi.etudid,
|
|
|
|
Justificatif.date_debut <= assi.date_debut,
|
|
|
|
Justificatif.date_fin >= assi.date_fin,
|
|
|
|
Justificatif.etat == EtatJustificatif.VALIDE,
|
|
|
|
).all()
|
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
# Si au moins un justificatif possède une période qui couvre l'assiduité
|
2023-07-30 16:34:05 +02:00
|
|
|
if any(
|
|
|
|
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
|
2023-09-24 09:18:13 +02:00
|
|
|
for j in justificatifs + assi_justificatifs
|
2023-07-30 16:34:05 +02:00
|
|
|
):
|
2023-10-27 16:05:40 +02:00
|
|
|
# On justifie l'assiduité
|
|
|
|
# On ajoute l'id de l'assiduité à la liste des assiduités justifiées
|
2023-04-17 15:34:00 +02:00
|
|
|
assi.est_just = True
|
2023-07-30 16:34:05 +02:00
|
|
|
assiduites_justifiees.append(assi.assiduite_id)
|
2023-04-17 15:34:00 +02:00
|
|
|
db.session.add(assi)
|
2023-07-30 16:34:05 +02:00
|
|
|
elif reset:
|
2023-10-27 16:05:40 +02:00
|
|
|
# Si le paramètre reset est Vrai alors les assiduités non justifiées
|
|
|
|
# sont remise en "non justifiée"
|
2023-04-17 15:34:00 +02:00
|
|
|
assi.est_just = False
|
|
|
|
db.session.add(assi)
|
2023-10-27 16:05:40 +02:00
|
|
|
# On valide la session
|
2023-04-17 15:34:00 +02:00
|
|
|
db.session.commit()
|
2023-10-27 16:05:40 +02:00
|
|
|
# On renvoie la liste des assiduite_id des assiduités justifiées
|
2023-07-30 16:34:05 +02:00
|
|
|
return assiduites_justifiees
|
2023-08-09 09:57:47 +02:00
|
|
|
|
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
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:
|
2023-12-07 12:38:47 +01:00
|
|
|
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
|
|
|
les identifiants, sinon les dict si long est vrai)
|
2023-10-27 16:05:40 +02:00
|
|
|
"""
|
2023-08-09 09:57:47 +02:00
|
|
|
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
|
2023-09-11 09:05:29 +02:00
|
|
|
return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
|
2023-08-09 09:57:47 +02:00
|
|
|
|
|
|
|
|
2023-09-11 09:05:29 +02:00
|
|
|
def get_justifs_from_date(
|
|
|
|
etudid: int,
|
|
|
|
date_debut: datetime,
|
|
|
|
date_fin: datetime,
|
|
|
|
long: bool = False,
|
|
|
|
valid: bool = False,
|
2023-10-27 16:05:40 +02:00
|
|
|
) -> 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:
|
2023-12-07 12:38:47 +01:00
|
|
|
list[int | dict]: La liste des justificatifs (par défaut uniquement
|
|
|
|
les identifiants, sinon les dict si long est vrai)
|
2023-10-27 16:05:40 +02:00
|
|
|
"""
|
|
|
|
# On récupère les justificatifs d'un étudiant couvrant la période donnée
|
2023-09-11 09:05:29 +02:00
|
|
|
justifs: Query = Justificatif.query.filter(
|
2023-08-09 09:57:47 +02:00
|
|
|
Justificatif.etudid == etudid,
|
|
|
|
Justificatif.date_debut <= date_debut,
|
|
|
|
Justificatif.date_fin >= date_fin,
|
|
|
|
)
|
|
|
|
|
2023-10-27 16:05:40 +02:00
|
|
|
# si valide est vrai alors on filtre pour n'avoir que les justificatifs valide
|
2023-09-11 09:05:29 +02:00
|
|
|
if valid:
|
|
|
|
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
|
|
|
|
|
2023-12-07 12:38:47 +01:00
|
|
|
# On renvoie la liste des id des justificatifs si long est Faux,
|
|
|
|
# sinon on renvoie les dicts des justificatifs
|
|
|
|
if long:
|
|
|
|
return [j.to_dict(True) for j in justifs]
|
|
|
|
return [j.justif_id for j in justifs]
|
2023-10-26 15:52:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
2023-10-27 16:05:40 +02:00
|
|
|
"""
|
|
|
|
get_formsemestre_from_data récupère un formsemestre en fonction des données passées
|
2023-12-06 14:42:10 +01:00
|
|
|
Si l'étudiant est inscrit à plusieurs formsemestre, prend le premier.
|
2023-10-27 16:05:40 +02:00
|
|
|
Args:
|
2023-12-07 12:38:47 +01:00
|
|
|
data (dict[str, datetime | int]): Une représentation simplifiée d'une
|
|
|
|
assiduité ou d'un justificatif
|
2023-10-27 16:05:40 +02:00
|
|
|
|
|
|
|
data = {
|
|
|
|
"etudid" : int,
|
|
|
|
"date_debut": datetime (tz),
|
|
|
|
"date_fin": datetime (tz),
|
|
|
|
}
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
FormSemestre: Le formsemestre trouvé ou None
|
|
|
|
"""
|
2023-10-26 15:52:53 +02:00
|
|
|
return (
|
|
|
|
FormSemestre.query.join(
|
|
|
|
FormSemestreInscription,
|
|
|
|
FormSemestre.id == FormSemestreInscription.formsemestre_id,
|
|
|
|
)
|
|
|
|
.filter(
|
|
|
|
data["date_debut"] <= FormSemestre.date_fin,
|
|
|
|
data["date_fin"] >= FormSemestre.date_debut,
|
|
|
|
FormSemestreInscription.etudid == data["etudid"],
|
|
|
|
)
|
|
|
|
.first()
|
|
|
|
)
|