# -*- coding: UTF-8 -* """Gestion de l'assiduité (assiduités + justificatifs) """ from datetime import datetime from app import db, log from app.models import ModuleImpl, Scolog from app.models.etudiants import Identite from app.auth.models import User from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_utils import ( EtatAssiduite, EtatJustificatif, localize_datetime, ) class Assiduite(db.Model): """ 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) description = db.Column(db.Text) 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) external_data = db.Column(db.JSON, nullable=True) # 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") def to_dict(self, format_api=True) -> dict: """Retourne la représentation json de l'assiduité""" etat = self.etat username = self.user_id if format_api: etat = EtatAssiduite.inverse().get(self.etat).name if self.user_id is not None: user: User = db.session.get(User, self.user_id) if user is None: username = "Non renseigné" else: username = user.get_prenomnom() data = { "assiduite_id": self.id, "etudid": self.etudid, "code_nip": self.etudiant.code_nip, "moduleimpl_id": self.moduleimpl_id, "date_debut": self.date_debut, "date_fin": self.date_fin, "etat": etat, "desc": self.description, "entry_date": self.entry_date, "user_id": username, "est_just": self.est_just, "external_data": self.external_data, } return data 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") }""" @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, external_data: dict = None, ) -> object or int: """Créer une nouvelle assiduité pour l'étudiant""" # Vérification de non duplication des périodes assiduites: list[Assiduite] = etud.assiduites if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite): raise ScoValueError( "Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)" ) if not est_just: est_just = ( len(_get_assiduites_justif(etud.etudid, date_debut, date_fin)) > 0 ) if moduleimpl is not None: # Vérification de l'existence du module pour l'étudiant if moduleimpl.est_inscrit(etud): nouv_assiduite = Assiduite( date_debut=date_debut, date_fin=date_fin, etat=etat, etudiant=etud, moduleimpl_id=moduleimpl.id, description=description, entry_date=entry_date, user_id=user_id, est_just=est_just, external_data=external_data, ) else: raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl") else: nouv_assiduite = Assiduite( date_debut=date_debut, date_fin=date_fin, etat=etat, etudiant=etud, description=description, entry_date=entry_date, user_id=user_id, est_just=est_just, external_data=external_data, ) db.session.add(nouv_assiduite) log(f"create_assiduite: {etud.id} {nouv_assiduite}") Scolog.logdb( method="create_assiduite", etudid=etud.id, msg=f"assiduité: {nouv_assiduite}", ) return nouv_assiduite class Justificatif(db.Model): """ 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()) 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()) # 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) def to_dict(self, format_api: bool = False) -> dict: """transformation de l'objet en dictionnaire sérialisable""" etat = self.etat username = self.user_id if format_api: etat = EtatJustificatif.inverse().get(self.etat).name if self.user_id is not None: user: User = db.session.get(User, self.user_id) if user is None: username = "Non renseigné" else: username = user.get_prenomnom() data = { "justif_id": self.justif_id, "etudid": self.etudid, "code_nip": self.etudiant.code_nip, "date_debut": self.date_debut, "date_fin": self.date_fin, "etat": etat, "raison": self.raison, "fichier": self.fichier, "entry_date": self.entry_date, "user_id": username, "external_data": self.external_data, } return data def __str__(self) -> str: "chaine pour journaux et debug (lisible par humain français)" try: etat_str = EtatJustificatif(self.etat).name except ValueError: etat_str = "Invalide" return f"""Justificatif {etat_str} de { self.date_debut.strftime("%d/%m/%Y %Hh%M") } à { self.date_fin.strftime("%d/%m/%Y %Hh%M") }""" @classmethod def create_justificatif( cls, etud: Identite, date_debut: datetime, date_fin: datetime, etat: EtatJustificatif, raison: str = None, entry_date: datetime = None, user_id: int = None, external_data: dict = None, ) -> object or int: """Créer un nouveau justificatif pour l'étudiant""" nouv_justificatif = Justificatif( date_debut=date_debut, date_fin=date_fin, etat=etat, etudiant=etud, raison=raison, entry_date=entry_date, user_id=user_id, external_data=external_data, ) db.session.add(nouv_justificatif) log(f"create_justificatif: {etud.id} {nouv_justificatif}") Scolog.logdb( method="create_justificatif", etudid=etud.id, msg=f"justificatif: {nouv_justificatif}", ) return nouv_justificatif def is_period_conflicting( date_debut: datetime, date_fin: datetime, collection: list[Assiduite or Justificatif], collection_cls: Assiduite or Justificatif, ) -> bool: """ Vérifie si une date n'entre pas en collision avec les justificatifs ou assiduites déjà présentes """ date_debut = localize_datetime(date_debut) date_fin = localize_datetime(date_fin) 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() return count > 0 def compute_assiduites_justified( etudid: int, justificatifs: list[Justificatif] = None, reset: bool = False ) -> list[int]: """ compute_assiduites_justified_faster 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. Returns: list[int]: la liste des assiduités qui ont été justifiées. """ if justificatifs is None: justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites_justifiees: list[int] = [] for assi in assiduites: if any( assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin for j in justificatifs ): assi.est_just = True assiduites_justifiees.append(assi.assiduite_id) db.session.add(assi) elif reset: assi.est_just = False db.session.add(assi) db.session.commit() return assiduites_justifiees def get_assiduites_justif(assiduite_id: int, long: bool): assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) return _get_assiduites_justif(assi.etudid, assi.date_debut, assi.date_fin, long) def _get_assiduites_justif( etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False ): justifs: Justificatif = Justificatif.query.filter( Justificatif.etudid == etudid, Justificatif.date_debut <= date_debut, Justificatif.date_fin >= date_fin, ) return [j.justif_id if not long else j.to_dict(True) for j in justifs]