diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index a3b3eb89..c9c4c00f 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -80,6 +80,9 @@ class BulletinBUT: """ res = self.res + if (etud.id, ue.id) in self.res.dispense_ues: + return {} + if ue.type == UE_SPORT: modimpls_spo = [ modimpl diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 698851e1..523cab3f 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -32,9 +32,17 @@ import pandas as pd from app import db from app import models -from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef +from app.models import ( + DispenseUE, + FormSemestre, + FormSemestreInscription, + Identite, + Module, + ModuleImpl, + ModuleUECoef, + UniteEns, +) from app.comp import moy_mod -from app.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT @@ -140,7 +148,8 @@ def df_load_modimpl_coefs( mod_coef.ue_id ] = mod_coef.coef except IndexError: - # il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation + # il peut y avoir en base des coefs sur des modules ou UE + # qui ont depuis été retirés de la formation pass # Initialisation des poids non fixés: # 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse @@ -199,7 +208,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: modimpls_results[modimpl.id] = mod_results modimpls_evals_poids[modimpl.id] = evals_poids modimpls_notes.append(etuds_moy_module) - if len(modimpls_notes): + if len(modimpls_notes) > 0: cube = notes_sem_assemble_cube(modimpls_notes) else: nb_etuds = formsemestre.etuds.count() @@ -211,14 +220,39 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: ) +def load_dispense_ues( + formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns] +) -> set[tuple[int, int]]: + """Construit l'ensemble des + etudids = modimpl_inscr_df.index, # les etudids + ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport + + Résultat: set de (etudid, ue_id). + """ + dispense_ues = set() + ue_sem_by_code = {ue.ue_code: ue for ue in ues} + # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, + # puis filtre sur inscrits et code d'UE UE + for dispense_ue in DispenseUE.query.join( + Identite, FormSemestreInscription + ).filter_by(formsemestre_id=formsemestre.id): + if dispense_ue.etudid in etudids: + # UE dans le semestre avec même code ? + ue = ue_sem_by_code.get(dispense_ue.ue.ue_code) + if ue is not None: + dispense_ues.add((dispense_ue.etudid, ue.id)) + + return dispense_ues + + def compute_ue_moys_apc( sem_cube: np.array, etuds: list, modimpls: list, - ues: list, modimpl_inscr_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame, modimpl_mask: np.array, + dispense_ues: set[tuple[int, int]], block: bool = False, ) -> pd.DataFrame: """Calcul de la moyenne d'UE en mode APC (BUT). @@ -230,7 +264,7 @@ def compute_ue_moys_apc( etuds : liste des étudiants (dim. 0 du cube) modimpls : liste des module_impl (dim. 1 du cube) ues : liste des UE (dim. 2 du cube) - modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) + modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl) modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas. (utilisé pour éliminer les bonus, et pourra servir à cacluler @@ -239,7 +273,6 @@ def compute_ue_moys_apc( Résultat: DataFrame columns UE (sans bonus), rows etudid """ nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape - nb_ues_tot = len(ues) assert len(modimpls) == nb_modules if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0: return pd.DataFrame( @@ -278,11 +311,16 @@ def compute_ue_moys_apc( etud_moy_ue = np.sum( modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) - return pd.DataFrame( + etud_moy_ue_df = pd.DataFrame( etud_moy_ue, index=modimpl_inscr_df.index, # les etudids columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport ) + # Les "dispenses" sont très peu nombreuses et traitées en python: + for dispense_ue in dispense_ues: + etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0 + + return etud_moy_ue_df def compute_ue_moys_classic( @@ -435,7 +473,7 @@ def compute_mat_moys_classic( Résultat: - moyennes: pd.Series, index etudid """ - if (not len(modimpl_mask)) or ( + if (0 == len(modimpl_mask)) or ( sem_matrix.shape[0] == 0 ): # aucun module ou aucun étudiant # etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 6ae6285f..bd889111 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -39,6 +39,7 @@ class ResultatsSemestreBUT(NotesTableCompat): """ndarray (etuds x modimpl x ue)""" self.etuds_parcour_id = None """Parcours de chaque étudiant { etudid : parcour_id }""" + if not self.load_cached(): t0 = time.time() self.compute() @@ -71,14 +72,17 @@ class ResultatsSemestreBUT(NotesTableCompat): modimpl.module.ue.type != UE_SPORT for modimpl in self.formsemestre.modimpls_sorted ] + self.dispense_ues = moy_ue.load_dispense_ues( + self.formsemestre, self.modimpl_inscr_df.index, self.ues + ) self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.sem_cube, self.etuds, self.formsemestre.modimpls_sorted, - self.ues, self.modimpl_inscr_df, self.modimpl_coefs_df, modimpls_mask, + self.dispense_ues, block=self.formsemestre.block_moyennes, ) # Les coefficients d'UE ne sont pas utilisés en APC diff --git a/app/comp/res_common.py b/app/comp/res_common.py index cd7e5f93..567c658d 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -48,12 +48,13 @@ class ResultatsSemestre(ResultatsCache): _cached_attrs = ( "bonus", "bonus_ues", + "dispense_ues", + "etud_coef_ue_df", "etud_moy_gen_ranks", "etud_moy_gen", "etud_moy_ue", "modimpl_inscr_df", "modimpls_results", - "etud_coef_ue_df", "moyennes_matieres", ) @@ -66,6 +67,8 @@ class ResultatsSemestre(ResultatsCache): "Bonus sur moy. gen. Series de float, index etudid" self.bonus_ues: pd.DataFrame = None # virtuel "DataFrame de float, index etudid, columns: ue.id" + self.dispense_ues: set[tuple[int, int]] = set() + """set des dispenses d'UE: (etudid, ue_id), en APC seulement.""" # ResultatsSemestreBUT ou ResultatsSemestreClassic self.etud_moy_ue = {} "etud_moy_ue: DataFrame columns UE, rows etudid" diff --git a/app/models/__init__.py b/app/models/__init__.py index 23c677c8..a832ad74 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -36,7 +36,7 @@ from app.models.etudiants import ( from app.models.events import Scolog, ScolarNews from app.models.formations import Formation, Matiere from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags -from app.models.ues import UniteEns +from app.models.ues import DispenseUE, UniteEns from app.models.formsemestre import ( FormSemestre, FormSemestreEtape, diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 89bb90de..5d7052af 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -58,6 +58,12 @@ class Identite(db.Model): billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") # admission = db.relationship("Admission", backref="identite", lazy="dynamic") + dispense_ues = db.relationship( + "DispenseUE", + back_populates="etud", + cascade="all, delete", + passive_deletes=True, + ) def __repr__(self): return ( diff --git a/app/models/ues.py b/app/models/ues.py index a832375b..964dfae2 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -5,6 +5,7 @@ from app import db, log from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models.but_refcomp import ApcNiveau, ApcParcours +from app.models.modules import Module from app.scodoc.sco_exceptions import ScoFormationConflict from app.scodoc import sco_utils as scu @@ -57,6 +58,12 @@ class UniteEns(db.Model): # relations matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue") + dispense_ues = db.relationship( + "DispenseUE", + back_populates="ue", + cascade="all, delete", + passive_deletes=True, + ) def __repr__(self): return f"""<{self.__class__.__name__}(id={self.id}, formation_id={ @@ -237,3 +244,31 @@ class UniteEns(db.Model): db.session.add(self) db.session.commit() log(f"ue.set_parcour( {self}, {parcour} )") + + +class DispenseUE(db.Model): + """Dispense d'UE + Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée + qu'ils ne refont pas. + """ + + __table_args__ = (db.UniqueConstraint("ue_id", "etudid"),) + id = db.Column(db.Integer, primary_key=True) + ue_id = db.Column( + db.Integer, + db.ForeignKey(UniteEns.id, ondelete="CASCADE"), + index=True, + nullable=False, + ) + ue = db.relationship("UniteEns", back_populates="dispense_ues") + etudid = db.Column( + db.Integer, + db.ForeignKey("identite.id", ondelete="CASCADE"), + index=True, + nullable=False, + ) + etud = db.relationship("Identite", back_populates="dispense_ues") + + def __repr__(self) -> str: + return f"""<{self.__class__.__name__} {self.id} etud={ + repr(self.etud)} ue={repr(self.ue)}>""" diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 09873bbe..d3e27dae 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -36,7 +36,7 @@ from flask_login import current_user from app import log -from app.models import ScolarNews +from app.models import ModuleImpl, ScolarNews from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -126,10 +126,13 @@ def do_evaluation_create( """Create an evaluation""" if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) args = locals() log("do_evaluation_create: args=" + str(args)) + modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) + if modimpl is None: + raise ValueError("module not found") check_evaluation_args(args) # Check numeros module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) @@ -172,16 +175,18 @@ def do_evaluation_create( r = _evaluationEditor.create(cnx, args) # news - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) - mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - mod["moduleimpl_id"] = M["moduleimpl_id"] - mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod + sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) + url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=moduleimpl_id, + ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=moduleimpl_id, - text='Création d\'une évaluation dans %(titre)s' % mod, - url=mod["url"], + text=f"""Création d'une évaluation dans { + modimpl.module.titre or '(module sans titre)'}""", + url=url, ) return r diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index a6d037ae..75aad0aa 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -262,6 +262,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id): """ authuser = current_user formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) is_apc = formsemestre.formation.is_apc() inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} @@ -390,65 +391,80 @@ def moduleimpl_inscriptions_stats(formsemestre_id): H.append("") # Etudiants "dispensés" d'une UE (capitalisée) - UECaps = get_etuds_with_capitalized_ue(formsemestre_id) - if UECaps: - H.append('

Etudiants avec UEs capitalisées: