# -*- coding: UTF-8 -* """ScoDoc models: moduleimpls """ import pandas as pd import flask_sqlalchemy from app import db from app.auth.models import User from app.comp import df_cache from app.models.etudiants import Identite from app.models.modules import Module from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu class ModuleImpl(db.Model): """Mise en oeuvre d'un module pour une annee/semestre""" __tablename__ = "notes_moduleimpl" __table_args__ = (db.UniqueConstraint("formsemestre_id", "module_id"),) id = db.Column(db.Integer, primary_key=True) moduleimpl_id = db.synonym("id") module_id = db.Column(db.Integer, db.ForeignKey("notes_modules.id"), nullable=False) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=False, ) responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id")) # formule de calcul moyenne: computation_expr = db.Column(db.Text()) evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl") enseignants = db.relationship( "User", secondary="notes_modules_enseignants", lazy="dynamic", backref="moduleimpl", viewonly=True, ) def __init__(self, **kwargs): super(ModuleImpl, self).__init__(**kwargs) def __repr__(self): return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" def get_evaluations_poids(self) -> pd.DataFrame: """Les poids des évaluations vers les UE (accès via cache)""" evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id) if evaluations_poids is None: from app.comp import moy_mod evaluations_poids, _ = moy_mod.load_evaluations_poids(self.id) df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids) return evaluations_poids def invalidate_evaluations_poids(self): """Invalide poids cachés""" df_cache.EvaluationsPoidsCache.delete(self.id) def check_apc_conformity(self, res: "ResultatsSemestreBUT") -> bool: """true si les poids des évaluations du module permettent de satisfaire les coefficients du PN. """ if not self.module.formation.get_cursus().APC_SAE or ( self.module.module_type != scu.ModuleType.RESSOURCE and self.module.module_type != scu.ModuleType.SAE ): return True # Non BUT, toujours conforme from app.comp import moy_mod return moy_mod.moduleimpl_is_conforme( self, self.get_evaluations_poids(), res.modimpl_coefs_df, ) def to_dict(self, convert_objects=False, with_module=True): """as a dict, with the same conversions as in ScoDoc7, including module. If convert_objects, convert all attributes to native types (suitable jor json encoding). """ d = dict(self.__dict__) d.pop("_sa_instance_state", None) if convert_objects: # on n'exporte pas le formsemestre et les inscriptions d.pop("formsemestre", None) d.pop("inscriptions", None) # ScoDoc7 output_formators: (backward compat) d["moduleimpl_id"] = self.id d["ens"] = [ {"moduleimpl_id": self.id, "ens_id": e.id} for e in self.enseignants ] if with_module: d["module"] = self.module.to_dict(convert_objects=convert_objects) else: d.pop("module", None) return d def can_change_ens_by(self, user: User, raise_exc=False) -> bool: """Check if user can modify module resp. If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not. = Admin, et dir des etud. (si option l'y autorise) """ if not self.formsemestre.etat: if raise_exc: raise ScoLockedSemError("Modification impossible: semestre verrouille") return False # -- check access # admin ou resp. semestre avec flag resp_can_change_resp if user.has_permission(Permission.ScoImplement): return True if ( user.id in [resp.id for resp in self.formsemestre.responsables] ) and self.formsemestre.resp_can_change_ens: return True if raise_exc: raise AccessDenied(f"Modification impossible pour {user}") return False def est_inscrit(self, etud: Identite) -> bool: """ Vérifie si l'étudiant est bien inscrit au moduleimpl Retourne Vrai si c'est le cas, faux sinon """ is_module: int = ( ModuleImplInscription.query.filter_by( etudid=etud.id, moduleimpl_id=self.id ).count() > 0 ) return is_module def can_change_ens_by(self, user: User, raise_exc=False) -> bool: """Check if user can modify module resp. If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not. = Admin, et dir des etud. (si option l'y autorise) """ if not self.formsemestre.etat: if raise_exc: raise ScoLockedSemError("Modification impossible: semestre verrouille") return False # -- check access # admin ou resp. semestre avec flag resp_can_change_resp if user.has_permission(Permission.ScoImplement): return True if ( user.id in [resp.id for resp in self.formsemestre.responsables] ) and self.formsemestre.resp_can_change_ens: return True if raise_exc: raise AccessDenied(f"Modification impossible pour {user}") return False # Enseignants (chargés de TD ou TP) d'un moduleimpl notes_modules_enseignants = db.Table( "notes_modules_enseignants", db.Column( "moduleimpl_id", db.Integer, db.ForeignKey("notes_moduleimpl.id", ondelete="CASCADE"), ), db.Column("ens_id", db.Integer, db.ForeignKey("user.id", ondelete="CASCADE")), # ? db.UniqueConstraint("moduleimpl_id", "ens_id"), ) # XXX il manque probablement une relation pour gérer cela class ModuleImplInscription(db.Model): """Inscription à un module (etudiants,moduleimpl)""" __tablename__ = "notes_moduleimpl_inscription" __table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),) id = db.Column(db.Integer, primary_key=True) moduleimpl_inscription_id = db.synonym("id") moduleimpl_id = db.Column( db.Integer, db.ForeignKey("notes_moduleimpl.id"), index=True, ) etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) etud = db.relationship( Identite, backref=db.backref("moduleimpl_inscriptions", cascade="all, delete-orphan"), ) modimpl = db.relationship( ModuleImpl, backref=db.backref("inscriptions", cascade="all, delete-orphan"), ) @classmethod def etud_modimpls_in_ue( cls, formsemestre_id: int, etudid: int, ue_id: int ) -> flask_sqlalchemy.BaseQuery: """moduleimpls de l'UE auxquels l'étudiant est inscrit. (Attention: inutile en APC, il faut considérer les coefficients) """ return ModuleImplInscription.query.filter( ModuleImplInscription.etudid == etudid, ModuleImplInscription.moduleimpl_id == ModuleImpl.id, ModuleImpl.formsemestre_id == formsemestre_id, ModuleImpl.module_id == Module.id, Module.ue_id == ue_id, ) @classmethod def nb_inscriptions_dans_ue( cls, formsemestre_id: int, etudid: int, ue_id: int ) -> int: """Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit""" return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count() @classmethod def sum_coefs_modimpl_ue( cls, formsemestre_id: int, etudid: int, ue_id: int ) -> float: """Somme des coefficients des modules auxquels l'étudiant est inscrit dans l'UE du semestre indiqué. N'utilise que les coefficients, donc inadapté aux formations APC. """ return sum( [ inscr.modimpl.module.coefficient for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id) ] )