ScoDoc-PE/app/models/moduleimpls.py

232 lines
8.2 KiB
Python

# -*- 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.ScoEditAllEvals)
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_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
# 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
) -> 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)
]
)