# -*- coding: UTF-8 -* """ScoDoc models: moduleimpls """ import pandas as pd from flask_sqlalchemy.query import Query 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_edit_evaluation(self, user) -> bool: """True if this user can create, delete or edit and evaluation in this modimpl (nb: n'implique pas le droit de saisir ou modifier des notes) """ # acces pour resp. moduleimpl et resp. form semestre (dir etud) if ( user.has_permission(Permission.EditAllEvals) or user.id == self.responsable_id or user.id in (r.id for r in self.formsemestre.responsables) ): return True elif self.formsemestre.ens_can_edit_eval: if user.id in (e.id for e in self.enseignants): return True return False def can_edit_notes(self, user: "User", allow_ens=True) -> bool: """True if authuser can enter or edit notes in this module. If allow_ens, grant access to all ens in this module Si des décisions de jury ont déjà été saisies dans ce semestre, seul le directeur des études peut saisir des notes (et il ne devrait pas). """ # was sco_permissions_check.can_edit_notes from app.scodoc import sco_cursus_dut if not self.formsemestre.etat: return False # semestre verrouillé is_dir_etud = user.id in (u.id for u in self.formsemestre.responsables) can_edit_all_notes = user.has_permission(Permission.EditAllNotes) if sco_cursus_dut.formsemestre_has_decisions(self.formsemestre_id): # il y a des décisions de jury dans ce semestre ! return can_edit_all_notes or is_dir_etud if ( not can_edit_all_notes and user.id != self.responsable_id and not is_dir_etud ): # enseignant (chargé de TD) ? return allow_ens and user.id in (ens.id for ens in self.enseignants) return True 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.EditFormSemestre): 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 # 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"), ) def to_dict(self) -> dict: "dict repr." return { "id": self.id, "etudid": self.etudid, "moduleimpl_id": self.moduleimpl_id, } @classmethod def etud_modimpls_in_ue( cls, formsemestre_id: int, etudid: int, ue_id: int ) -> Query: """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) ] )