diff --git a/app/comp/df_cache.py b/app/comp/df_cache.py new file mode 100644 index 00000000..dec325e2 --- /dev/null +++ b/app/comp/df_cache.py @@ -0,0 +1,49 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""caches pour tables APC +""" + +from app.scodoc import sco_cache + + +class ModuleCoefsCache(sco_cache.ScoDocCache): + """Cache for module coefs + Clé: formation_id.semestre_idx + Valeur: DataFrame (df_load_module_coefs) + """ + + prefix = "MCO" + + +class EvaluationsPoidsCache(sco_cache.ScoDocCache): + """Cache for poids evals + Clé: moduleimpl_id + Valeur: DataFrame (df_load_evaluations_poids) + """ + + prefix = "EPC" diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index e1cc4a16..20c85b97 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -38,7 +38,7 @@ from app.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours -def df_load_module_coefs(formation_id: int, semestre_idx: int) -> pd.DataFrame: +def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame: """Charge les coefs des modules de la formation pour le semestre indiqué. Ces coefs lient les modules à chaque UE. diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 5921dddc..8c093d57 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -52,6 +52,7 @@ class Evaluation(db.Model): e = dict(self.__dict__) e.pop("_sa_instance_state", None) # ScoDoc7 output_formators + e["evaluation_id"] = self.id e["jour"] = ndb.DateISOtoDMY(e["jour"]) e["numero"] = ndb.int_null_is_zero(e["numero"]) return sco_evaluation_db.evaluation_enrich_dict(e) @@ -88,6 +89,7 @@ class Evaluation(db.Model): ue = UniteEns.query.get(ue_id) L.append(EvaluationUEPoids(evaluation=self, ue=ue, poids=poids)) self.ue_poids = L + self.moduleimpl.invalidate_evaluations_poids() # inval cache def update_ue_poids_dict(self, ue_poids_dict: dict): """update poids vers UE (ajoute aux existants)""" diff --git a/app/models/formations.py b/app/models/formations.py index 7994a40f..942e1a9b 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -9,6 +9,7 @@ from app.scodoc import notesdb as ndb from app.scodoc import sco_utils as scu from app.scodoc.sco_utils import ModuleType from app.scodoc import sco_codes_parcours +from app.comp import df_cache class Formation(db.Model): @@ -46,6 +47,34 @@ class Formation(db.Model): """get l'instance de TypeParcours de cette formation""" return sco_codes_parcours.get_parcours_from_code(self.type_parcours) + def get_module_coefs(self, semestre_idx: int = None): + """Les coefs des modules vers les UE (accès via cache)""" + from app.comp import moy_ue + + if semestre_idx is None: + key = f"{self.id}" + else: + key = f"{self.id}.{semestre_idx}" + + modules_coefficients = df_cache.ModuleCoefsCache.get(key) + if modules_coefficients is None: + modules_coefficients, _, _ = moy_ue.df_load_module_coefs( + self.id, semestre_idx + ) + df_cache.ModuleCoefsCache.set(key, modules_coefficients) + return modules_coefficients + + def invalidate_module_coefs(self, semestre_idx: int = None): + """Invalide les coefficients de modules cachés. + Si semestre_idx est None, invalide tous les semestres, + sinon invalide le semestre indiqué et le cache de la formation. + """ + if semestre_idx is None: + keys = {f"{self.id}.{m.semestre_id}" for m in self.modules} + else: + keys = f"{self.id}.{semestre_idx}" + df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"}) + class UniteEns(db.Model): """Unité d'Enseignement (UE)""" @@ -186,6 +215,13 @@ class Module(db.Model): f"" ) + def is_apc(self): + "True si module SAÉ ou Ressource" + return scu.ModuleType(self.module_type) in { + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, + } + def type_name(self): return scu.MODULE_TYPE_NAMES[self.module_type] @@ -206,6 +242,7 @@ class Module(db.Model): else: ue_coefs.append(ModuleUECoef(module=self, ue=ue, coef=coef)) self.ue_coefs = ue_coefs + self.formation.invalidate_module_coefs() def update_ue_coef_dict(self, ue_coef_dict: dict): """update coefs vers UE (ajoute aux existants)""" @@ -222,6 +259,7 @@ class Module(db.Model): ue_coef = ModuleUECoef.query.get((self.id, ue.id)) if ue_coef: db.session.delete(ue_coef) + self.formation.invalidate_module_coefs() def ue_coefs_descr(self): """List of tuples [ (ue_acronyme, coef) ]""" diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 8b97ed74..ea4374e6 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -4,7 +4,10 @@ """ from typing import Any +import pandas as pd + from app import db +from app.comp import df_cache from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN @@ -36,6 +39,55 @@ class ModuleImpl(db.Model): 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 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.df_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) -> bool: + """true si les poids des évaluations du module permettent de satisfaire + les coefficients du PN. + """ + if not self.module.formation.get_parcours().APC_SAE: + return True + from app.comp import moy_mod + + return moy_mod.check_moduleimpl_conformity( + self, + self.get_evaluations_poids(), + self.module.formation.get_module_coefs(self.module.semestre_id), + ) + + def to_dict(self): + """as a dict, with the same conversions as in ScoDoc7""" + e = dict(self.__dict__) + e.pop("_sa_instance_state", None) + # ScoDoc7 output_formators: (backward compat) + e["moduleimpl_id"] = self.id + e["ens"] = [ + {"moduleimpl_id": self.id, "ens_id": e.id} for e in self.enseignants + ] + return e # Enseignants (chargés de TD ou TP) d'un moduleimpl @@ -70,7 +122,7 @@ class ModuleImplInscription(db.Model): Identite, backref=db.backref("moduleimpl_inscriptions", cascade="all, delete-orphan"), ) - moduleimpl = db.relationship( + modimpl = db.relationship( ModuleImpl, backref=db.backref("inscriptions", cascade="all, delete-orphan"), ) diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 9c20a213..cab60d09 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -29,7 +29,7 @@ Ré-écrite pour ScoDoc8, utilise flask_caching et REDIS - ScoDoc est maintenant multiprocessus / mono-thread, avec un cache en mémoire partagé. + ScoDoc est maintenant multiprocessus / mono-thread, avec un cache partagé. """ diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index bbaf29d1..28de6d66 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -33,6 +33,7 @@ from flask import g, url_for from flask_login import current_user from app.auth.models import User +from app.models import ModuleImpl import app.scodoc.sco_utils as scu from app.scodoc.sco_permissions import Permission @@ -156,7 +157,8 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0): def moduleimpl_status(moduleimpl_id=None, partition_id=None): """Tableau de bord module (liste des evaluations etc)""" - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] + modimpl = ModuleImpl.query.get_or_404(moduleimpl_id) + M = modimpl.to_dict() formsemestre_id = M["formsemestre_id"] Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] sem = sco_formsemestre.get_formsemestre(formsemestre_id) @@ -224,10 +226,17 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): H.append("""""") if not sem["etat"]: H.append(scu.icontag("lock32_img", title="verrouillé")) - H.append( - """Coef dans le semestre: %(coefficient)s""" - % Mod - ) + H.append("""Coef dans le semestre: """) + if modimpl.module.is_apc(): + coefs_descr = modimpl.module.ue_coefs_descr() + if coefs_descr: + coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr]) + else: + coefs_descr_txt = """non définis""" + H.append(coefs_descr_txt) + else: + H.append(f"{modimpl.module.coefficient}") + H.append("""""") # 3ieme ligne: Formation H.append( """Formation: %(titre)s""" % F @@ -289,6 +298,13 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): H.append("") # + if not modimpl.check_apc_conformity(): + H.append( + """""" + ) + # if has_expression and nt.expr_diagnostics: H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics)) #