diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 61ee57df15..a62e9c0570 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -37,13 +37,14 @@ class ResultatsSemestreBUT: "modimpls_evals_notes", "etud_moy_gen", "etud_moy_gen_ranks", + "modimpls_evaluations_complete", ) def __init__(self, formsemestre): self.formsemestre = formsemestre self.ues = formsemestre.query_ues().all() self.modimpls = formsemestre.modimpls.all() - self.etuds = self.formsemestre.etuds.all() + self.etuds = self.formsemestre.get_inscrits(include_dem=False) self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)} self.saes = [ m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE @@ -77,7 +78,8 @@ class ResultatsSemestreBUT: self.sem_cube, self.modimpls_evals_poids, self.modimpls_evals_notes, - _, + modimpls_evaluations, + self.modimpls_evaluations_complete, ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( @@ -151,10 +153,17 @@ class ResultatsSemestreBUT: for mi in modimpls: mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id) # moyennes indicatives (moyennes de moyennes d'UE) - moyennes_etuds = np.nan_to_num( - np.nanmean(self.sem_cube[:, mod_idx, :], axis=1), - copy=False, - ) + try: + moyennes_etuds = np.nan_to_num( + np.nanmean(self.sem_cube[:, mod_idx, :], axis=1), + copy=False, + ) + except RuntimeWarning: # all nans in np.nanmean (sur certains etuds sans notes valides) + pass + try: + moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx]) + except RuntimeWarning: # all nans in np.nanmean + pass d[mi.module.code] = { "id": mi.id, "titre": mi.module.titre, @@ -166,15 +175,16 @@ class ResultatsSemestreBUT: ), "moyenne": { # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan) - "value": fmt_note(np.nanmean(self.sem_cube[etud_idx, mod_idx])), + "value": fmt_note(moy_indicative_mod), "min": fmt_note(moyennes_etuds.min()), "max": fmt_note(moyennes_etuds.max()), "moy": fmt_note(moyennes_etuds.mean()), }, "evaluations": [ self.etud_eval_results(etud, e) - for e in mi.evaluations + for eidx, e in enumerate(mi.evaluations) if e.visibulletin + and self.modimpls_evaluations_complete[mi.id][eidx] ], } return d diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 10da10e21b..ea34ecb072 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -89,7 +89,7 @@ def check_moduleimpl_conformity( def df_load_modimpl_notes(moduleimpl_id: int) -> tuple: - """Construit un dataframe avec toutes les notes des évaluations du module. + """Construit un dataframe avec toutes les notes de toutes les évaluations du module. colonnes: le nom de la colonne est l'evaluation_id (int) index (lignes): etudid (int) @@ -111,9 +111,12 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple: N'utilise pas de cache ScoDoc. """ - # L'index du dataframe est la liste des étudiants inscrits au semestre: + # L'index du dataframe est la liste des étudiants inscrits au semestre, sans les démissionnaires etudids = [ - e.etudid for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.inscriptions + e.etudid + for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits( + include_dem=False + ) ] evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() if evaluations: @@ -138,9 +141,10 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple: index_col="etudid", dtype=np.float64, ) - evaluations_completes.append( + is_complete = ( len(eval_df) == nb_inscrits_module or evaluation.publish_incomplete ) + evaluations_completes.append(is_complete) # NULL en base => ABS (= -999) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) # Ce merge met à NULL les élements non présents diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 90f6a25017..74994f26d3 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -136,10 +136,12 @@ def notes_sem_load_cube(formsemestre): modimpls_evals_poids dict { modimpl.id : evals_poids } modimpls_evals_notes dict { modimpl.id : evals_notes } modimpls_evaluations dict { modimpl.id : liste des évaluations } + modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)} """ modimpls_evals_poids = {} modimpls_evals_notes = {} modimpls_evaluations = {} + modimpls_evaluations_complete = {} modimpls_notes = [] for modimpl in formsemestre.modimpls: evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( @@ -152,12 +154,14 @@ def notes_sem_load_cube(formsemestre): modimpls_evals_poids[modimpl.id] = evals_poids modimpls_evals_notes[modimpl.id] = evals_notes modimpls_evaluations[modimpl.id] = evaluations + modimpls_evaluations_complete[modimpl.id] = evaluations_completes modimpls_notes.append(etuds_moy_module) return ( notes_sem_assemble_cube(modimpls_notes), modimpls_evals_poids, modimpls_evals_notes, modimpls_evaluations, + modimpls_evaluations_complete, ) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 13a71c941e..cddac171ce 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -5,6 +5,7 @@ """ from app import db +from app import models from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN @@ -98,6 +99,18 @@ class Identite(db.Model): ] return r[0] if r else None + def etat_inscription(self, formsemestre_id): + """etat de l'inscription de cet étudiant au semestre: + False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF + """ + # voir si ce n'est pas trop lent: + ins = models.FormsemestreInscription.query.filter_by( + etudid=self.id, formsemestre_id=formsemestre_id + ).first() + if ins: + return ins.etat + return False + class Adresse(db.Model): """Adresse d'un étudiant diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 7126199aec..105a98eef9 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -240,6 +240,15 @@ class FormSemestre(db.Model): etudid, self.date_debut.isoformat(), self.date_fin.isoformat() ) + def get_inscrits(self, include_dem=False) -> list: + """Liste des étudiants inscrits à ce semestre + Si all, tous les étudiants, avec les démissionnaires. + """ + if include_dem: + return [ins.etud for ins in self.inscriptions] + else: + return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT] + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( @@ -386,10 +395,11 @@ class FormsemestreInscription(db.Model): id = db.Column(db.Integer, primary_key=True) formsemestre_inscription_id = db.synonym("id") - etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) + etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True) formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), + index=True, ) etud = db.relationship( Identite, @@ -400,7 +410,7 @@ class FormsemestreInscription(db.Model): backref=db.backref("inscriptions", cascade="all, delete-orphan"), ) # I inscrit, D demission en cours de semestre, DEF si "defaillant" - etat = db.Column(db.String(CODE_STR_LEN)) + etat = db.Column(db.String(CODE_STR_LEN), index=True) # etape apogee d'inscription (experimental 2020) etape = db.Column(db.String(APO_CODE_STR_LEN)) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index c023c738f9..a9f0552a83 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -71,6 +71,12 @@ NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes NOTES_SUPPRESS = -1001.0 # note a supprimer NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) +# ---- CODES INSCRIPTION AUX SEMESTRES +# (champ etat de FormsemestreInscription) +INSCRIT = "I" +DEMISSION = "D" +DEF = "DEF" + # Types de modules class ModuleType(IntEnum): """Code des types de module.""" diff --git a/app/static/css/bulletin-but.css b/app/static/css/bulletin-but.css index 0ef83dd5ed..76b452bd0e 100644 --- a/app/static/css/bulletin-but.css +++ b/app/static/css/bulletin-but.css @@ -7,7 +7,7 @@ --couleurFondTitresUE: rgb(206,255,235); --couleurFondTitresRes: rgb(125, 170, 255); --couleurFondTitresSAE: rgb(255, 190, 69); - --couleurSecondaire: #0c9; + --couleurSecondaire: #fec; --couleurIntense: #c09; --couleurSurlignage: rgba(232, 255, 132, 0.47); } @@ -132,7 +132,7 @@ section>div:nth-child(1){ } .enteteSemestre{ - color: var(--couleurSecondaire); + color: black; font-weight: bold; font-size: 20px; margin-bottom: 4px; @@ -157,7 +157,7 @@ section>div:nth-child(1){ /***************/ /* Evaluations */ /***************/ -.module, .ue{ +.module, .ue { background: var(--couleurSecondaire); color: #000; padding: 4px 32px; @@ -167,6 +167,9 @@ section>div:nth-child(1){ margin: 4px 0 2px 0; overflow: auto; } +.sae .module, .sae .module h3 { + background: #d3ffff; +} h3{ display: flex; align-items: center; diff --git a/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py b/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py new file mode 100644 index 0000000000..179ed98cf4 --- /dev/null +++ b/migrations/versions/4f98a8b02c89_index_in_formsemestreinscription.py @@ -0,0 +1,32 @@ +"""index in FormsemestreInscription + +Revision ID: 4f98a8b02c89 +Revises: a57a6ee2e3cb +Create Date: 2021-12-14 16:56:11.034680 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4f98a8b02c89' +down_revision = 'a57a6ee2e3cb' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index(op.f('ix_notes_formsemestre_inscription_etat'), 'notes_formsemestre_inscription', ['etat'], unique=False) + op.create_index(op.f('ix_notes_formsemestre_inscription_etudid'), 'notes_formsemestre_inscription', ['etudid'], unique=False) + op.create_index(op.f('ix_notes_formsemestre_inscription_formsemestre_id'), 'notes_formsemestre_inscription', ['formsemestre_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_notes_formsemestre_inscription_formsemestre_id'), table_name='notes_formsemestre_inscription') + op.drop_index(op.f('ix_notes_formsemestre_inscription_etudid'), table_name='notes_formsemestre_inscription') + op.drop_index(op.f('ix_notes_formsemestre_inscription_etat'), table_name='notes_formsemestre_inscription') + # ### end Alembic commands ### diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py index 2bf03fca42..d27463f409 100644 --- a/tests/unit/test_but_ues.py +++ b/tests/unit/test_but_ues.py @@ -69,7 +69,7 @@ def test_ue_moy(test_client): _ = sco_saisie_notes.notes_add(G.default_user, evaluation1.id, [(etudid, n1)]) _ = sco_saisie_notes.notes_add(G.default_user, evaluation2.id, [(etudid, n2)]) # Recalcul des moyennes - sem_cube, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) + sem_cube, _, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) etuds = formsemestre.etuds.all() etud_moy_ue = moy_ue.compute_ue_moys( sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df @@ -112,7 +112,7 @@ def test_ue_moy(test_client): exception_raised = True assert exception_raised # Recalcule les notes: - sem_cube, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) + sem_cube, _, _, _, _ = moy_ue.notes_sem_load_cube(formsemestre) etuds = formsemestre.etuds.all() etud_moy_ue = moy_ue.compute_ue_moys( sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df