# -*- coding: UTF-8 -* """ScoDoc models: moduleimpls """ import pandas as pd from flask import abort, g from flask_login import current_user 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 import APO_CODE_STR_LEN, ScoDocModel from app.models.etudiants import Identite from app.models.evaluations import Evaluation 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(ScoDocModel): """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) code_apogee = db.Column(db.String(APO_CODE_STR_LEN), index=True, nullable=True) "id de l'element pedagogique Apogee correspondant" edt_id: str | None = db.Column(db.Text(), index=True, nullable=True) "identifiant emplois du temps (unicité non imposée)" 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", ondelete="SET NULL") ) responsable = db.relationship("User", back_populates="modimpls") # formule de calcul moyenne: computation_expr = db.Column(db.Text()) evaluations = db.relationship( "Evaluation", lazy="dynamic", backref="moduleimpl", order_by=(Evaluation.numero, Evaluation.date_debut), ) "évaluations, triées par numéro et dates croissants, donc la plus ancienne d'abord." enseignants = db.relationship( "User", secondary="notes_modules_enseignants", lazy="dynamic", backref="moduleimpl", ) "enseignants du module (sans le responsable)" def __repr__(self): return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" def get_codes_apogee(self) -> set[str]: """Les codes Apogée (codés en base comme "VRT1,VRT2"). (si non renseigné, ceux du module) """ if self.code_apogee: return {x.strip() for x in self.code_apogee.split(",") if x} return self.module.get_codes_apogee() def get_edt_ids(self) -> list[str]: "les ids pour l'emploi du temps: à défaut, les codes Apogée" return [ scu.normalize_edt_id(x) for x in scu.split_id(self.edt_id) or scu.split_id(self.code_apogee) ] or self.module.get_edt_ids() 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 @classmethod def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl": """ModuleImpl ou 404, cherche uniquement dans le département spécifié ou le courant.""" from app.models.formsemestre import FormSemestre if not isinstance(moduleimpl_id, int): try: moduleimpl_id = int(moduleimpl_id) except (TypeError, ValueError): abort(404, "moduleimpl_id invalide") if g.scodoc_dept: dept_id = dept_id if dept_id is not None else g.scodoc_dept_id query = cls.query.filter_by(id=moduleimpl_id) if dept_id is not None: query = query.join(FormSemestre).filter_by(dept_id=dept_id) return query.first_or_404() 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) d["code_apogee"] = d["code_apogee"] or "" # pas de 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_responsable(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 can_change_ens(self, user: User | None = None, raise_exc=True) -> bool: """check if user can modify ens list (raise exception if not)" if user is None, current user. """ user = current_user if user is None else user if not self.formsemestre.etat: if raise_exc: raise ScoLockedSemError("Modification impossible: semestre verrouille") return False # -- check access # admin, resp. module ou resp. semestre if ( user.id != self.responsable_id and not user.has_permission(Permission.EditFormSemestre) and user.id not in (u.id for u in self.formsemestre.responsables) ): if raise_exc: raise AccessDenied(f"Modification impossible pour {user}") return False return True def est_inscrit(self, etud: Identite) -> bool: """ Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre). (lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df). Retourne Vrai si inscrit au module, 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) ] )