From 878ea41933d7e6d3eb76252f6f6792184825a935 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 18 May 2022 20:43:01 +0200 Subject: [PATCH] Ajout groupes et rangs/groupes aux bulletins BUT --- app/but/bulletin_but.py | 49 ++++++++++++++++--- app/comp/res_common.py | 3 +- app/comp/res_compat.py | 80 +++++++++++++++++++++++++------- app/models/etudiants.py | 4 +- app/models/groups.py | 33 +++++++++++++ app/scodoc/sco_bulletins.py | 4 +- app/scodoc/sco_bulletins_json.py | 2 +- app/scodoc/sco_bulletins_xml.py | 2 +- app/scodoc/sco_groups.py | 36 ++++++++++++-- 9 files changed, 179 insertions(+), 34 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 689ea9ed97..f9dbb87062 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -14,10 +14,12 @@ from flask import url_for, g from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre, Identite +from app.models.groups import GroupDescr from app.models.ues import UniteEns from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_pdf +from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_utils import fmt_note @@ -64,8 +66,16 @@ class BulletinBUT: # } return d - def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict: - "dict synthèse résultats UE" + def etud_ue_results( + self, + etud: Identite, + ue: UniteEns, + decision_ue: dict, + etud_groups: list[GroupDescr] = None, + ) -> dict: + """dict synthèse résultats UE + etud_groups : liste des groupes, pour affichage du rang. + """ res = self.res d = { @@ -81,7 +91,7 @@ class BulletinBUT: if res.bonus_ues is not None and ue.id in res.bonus_ues else fmt_note(0.0), "malus": fmt_note(res.malus[ue.id][etud.id]), - "capitalise": None, # "AAAA-MM-JJ" TODO #sco92 + "capitalise": None, # "AAAA-MM-JJ" TODO #sco93 "ressources": self.etud_ue_mod_results(etud, ue, res.ressources), "saes": self.etud_ue_mod_results(etud, ue, res.saes), } @@ -103,7 +113,18 @@ class BulletinBUT: "moy": fmt_note(res.etud_moy_ue[ue.id].mean()), "rang": rang, "total": effectif, # nb etud avec note dans cette UE + "groupes": {}, } + if self.prefs["bul_show_ue_rangs"]: + for group in etud_groups: + if group.partition.bul_show_rank: + rang, effectif = self.res.get_etud_ue_rang( + ue.id, etud.id, group.id + ) + d["moyenne"]["groupes"][group.id] = { + "value": rang, + "total": effectif, + } else: # ceci suppose que l'on a une seule UE bonus, # en tous cas elles auront la même description @@ -275,6 +296,9 @@ class BulletinBUT: return d nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) + etud_groups = sco_groups.get_etud_formsemestre_groups( + etud, formsemestre, only_to_show=True + ) semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "date_debut": formsemestre.date_debut.isoformat(), @@ -282,7 +306,7 @@ class BulletinBUT: "annee_universitaire": formsemestre.annee_scolaire_str(), "numero": formsemestre.semestre_id, "inscription": "", # inutilisé mais nécessaire pour le js de Seb. - "groupes": [], # XXX TODO + "groupes": [group.to_dict() for group in etud_groups], } if self.prefs["bul_show_abs"]: semestre_infos["absences"] = { @@ -306,15 +330,25 @@ class BulletinBUT: "max": fmt_note(res.etud_moy_gen.max()), } if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]): - # classement wrt moyenne général, indicatif + # classement wrt moyenne générale, indicatif semestre_infos["rang"] = { "value": res.etud_moy_gen_ranks[etud.id], "total": nb_inscrits, + "groupes": {}, } + # Rangs par groupes + for group in etud_groups: + if group.partition.bul_show_rank: + rang, effectif = self.res.get_etud_rang_group(etud.id, group.id) + semestre_infos["rang"]["groupes"][group.id] = { + "value": rang, + "total": effectif, + } else: semestre_infos["rang"] = { "value": "-", "total": nb_inscrits, + "groupes": {}, } d.update( { @@ -324,7 +358,10 @@ class BulletinBUT: "saes": self.etud_mods_results(etud, res.saes, version=version), "ues": { ue.acronyme: self.etud_ue_results( - etud, ue, decision_ue=decisions_ues.get(ue.id, {}) + etud, + ue, + decision_ue=decisions_ues.get(ue.id, {}), + etud_groups=etud_groups, ) for ue in res.ues # si l'UE comporte des modules auxquels on est inscrit: diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7bc199ead4..5170875d71 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -18,7 +18,7 @@ from app.auth.models import User from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef, formsemestre +from app.models import FormSemestre, FormSemestreUECoef from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns @@ -151,6 +151,7 @@ class ResultatsSemestre(ResultatsCache): if m.module.module_type == scu.ModuleType.SAE ] + # --- JURY... def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: """Liste des UEs du semestre qui doivent être validées diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 8bbed09041..5ac18ff4ec 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -35,7 +35,9 @@ class NotesTableCompat(ResultatsSemestre): "malus", "etud_moy_gen_ranks", "etud_moy_gen_ranks_int", + "moy_gen_rangs_by_group", "ue_rangs", + "ue_rangs_by_group", ) def __init__(self, formsemestre: FormSemestre): @@ -48,6 +50,8 @@ class NotesTableCompat(ResultatsSemestre): self.moy_min = "NA" self.moy_max = "NA" self.moy_moy = "NA" + self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) } + self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}} self.expr_diagnostics = "" self.parcours = self.formsemestre.formation.get_parcours() @@ -153,31 +157,83 @@ class NotesTableCompat(ResultatsSemestre): def compute_rangs(self): """Calcule les classements Moyenne générale: etud_moy_gen_ranks - Par UE (sauf ue bonus) + Par UE (sauf ue bonus): ue_rangs[ue.id] + Par groupe: classements selon moy_gen et UE: + moy_gen_rangs_by_group[group_id] + ue_rangs_by_group[group_id] """ ( self.etud_moy_gen_ranks, self.etud_moy_gen_ranks_int, ) = moy_sem.comp_ranks_series(self.etud_moy_gen) - for ue in self.formsemestre.query_ues(): + ues = self.formsemestre.query_ues() + for ue in ues: moy_ue = self.etud_moy_ue[ue.id] self.ue_rangs[ue.id] = ( moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine int(moy_ue.count()), ) # .count() -> nb of non NaN values + # Rangs dans les groupes (moy. gen et par UE) + self.moy_gen_rangs_by_group = {} # { group_id : (Series, Series) } + self.ue_rangs_by_group = {} + partitions_avec_rang = self.formsemestre.partitions.filter_by( + bul_show_rank=True + ) + for partition in partitions_avec_rang: + for group in partition.groups: + # on prend l'intersection car les groupes peuvent inclure des étudiants désinscrits + group_members = list( + {etud.id for etud in group.etuds}.intersection( + self.etud_moy_gen.index + ) + ) + # list() car pandas veut une sequence pour take() + # Rangs / moyenne générale: + group_moys_gen = self.etud_moy_gen[group_members] + self.moy_gen_rangs_by_group[group.id] = moy_sem.comp_ranks_series( + group_moys_gen + ) + # Rangs / UEs: + for ue in ues: + group_moys_ue = self.etud_moy_ue[ue.id][group_members] + self.ue_rangs_by_group.setdefault(ue.id, {})[ + group.id + ] = moy_sem.comp_ranks_series(group_moys_ue) - def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]: + def get_etud_rang(self, etudid: int) -> str: + """Le rang (classement) de l'étudiant dans le semestre. + Result: "13" ou "12 ex" + """ + return self.etud_moy_gen_ranks.get(etudid, 99999) + + def get_etud_ue_rang(self, ue_id, etudid, group_id=None) -> tuple[str, int]: """Le rang de l'étudiant dans cette ue + Si le group_id est spécifié, rang au sein de ce groupe, sinon global. Result: rang:str, effectif:str """ - rangs, effectif = self.ue_rangs[ue_id] - if rangs is not None: - rang = rangs[etudid] + if group_id is None: + rangs, effectif = self.ue_rangs[ue_id] + if rangs is not None: + rang = rangs[etudid] + else: + return "", "" else: - return "", "" + rangs = self.ue_rangs_by_group[ue_id][group_id][0] + rang = rangs[etudid] + effectif = len(rangs) return rang, effectif + def get_etud_rang_group(self, etudid: int, group_id: int) -> tuple[str, int]: + """Rang de l'étudiant (selon moy gen) et effectif dans ce groupe. + Si le groupe n'a pas de rang (partition avec bul_show_rank faux), ramène "", 0 + """ + if group_id in self.moy_gen_rangs_by_group: + r = self.moy_gen_rangs_by_group[group_id][0] # version en str + return (r[etudid], len(r)) + else: + return "", 0 + def etud_check_conditions_ues(self, etudid): """Vrai si les conditions sur les UE sont remplies. Ne considère que les UE ayant des notes (moyenne calculée). @@ -298,16 +354,6 @@ class NotesTableCompat(ResultatsSemestre): "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) } - def get_etud_rang(self, etudid: int) -> str: - """Le rang (classement) de l'étudiant dans le semestre. - Result: "13" ou "12 ex" - """ - return self.etud_moy_gen_ranks.get(etudid, 99999) - - def get_etud_rang_group(self, etudid: int, group_id: int): - "Le rang de l'étudiant dans ce groupe (NON IMPLEMENTE)" - return (None, 0) # XXX unimplemented TODO - def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: """Liste d'informations (compat NotesTable) sur évaluations completes de ce module. diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 2e9292c1db..0bce6d47e5 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -56,11 +56,11 @@ class Identite(db.Model): # adresses = db.relationship("Adresse", lazy="dynamic", backref="etud") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") - # one-to-one relation: + # admission = db.relationship("Admission", backref="identite", lazy="dynamic") def __repr__(self): - return f"" + return f"" @classmethod def from_request(cls, etudid=None, code_nip=None): diff --git a/app/models/groups.py b/app/models/groups.py index 9cf5f23647..4c64ad543b 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -25,9 +25,11 @@ class Partition(db.Model): partition_name = db.Column(db.String(SHORT_STR_LEN)) # numero = ordre de presentation) numero = db.Column(db.Integer) + # Calculer le rang ? bul_show_rank = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) + # Montrer quand on indique les groupes de l'étudiant ? show_in_lists = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) @@ -50,6 +52,18 @@ class Partition(db.Model): def __repr__(self): return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">""" + def to_dict(self, with_groups=False) -> dict: + """as a dict, with or without groups""" + d = { + "id": self.id, + "formsemestre_id": self.partition_id, + "name": self.partition_name, + "numero": self.numero, + } + if with_groups: + d["groups"] = [group.to_dict(with_partition=False) for group in self.groups] + return d + class GroupDescr(db.Model): """Description d'un groupe d'une partition""" @@ -78,6 +92,17 @@ class GroupDescr(db.Model): "Nom avec partition: 'TD A'" return f"{self.partition.partition_name or ''} {self.group_name or '-'}" + def to_dict(self, with_partition=True) -> dict: + """as a dict, with or without partition""" + d = { + "id": self.id, + "partition_id": self.partition_id, + "name": self.group_name, + } + if with_partition: + d["partition"] = self.partition.to_dict(with_groups=False) + return d + group_membership = db.Table( "group_membership", @@ -85,3 +110,11 @@ group_membership = db.Table( db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.UniqueConstraint("etudid", "group_id"), ) +# class GroupMembership(db.Model): +# """Association groupe / étudiant""" + +# __tablename__ = "group_membership" +# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) +# id = db.Column(db.Integer, primary_key=True) +# etudid = db.Column(db.Integer, db.ForeignKey("identite.id")) +# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id")) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index efc0b53415..df790649a6 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -251,7 +251,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): rang = "" rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) if nt.get_moduleimpls_attente(): @@ -651,7 +651,7 @@ def _ue_mod_bulletin( def get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid: int, partitions, partitions_etud_groups, nt: NotesTableCompat ): """Ramene rang et nb inscrits dans chaque partition""" rang_gr, ninscrits_gr, gr_name = {}, {}, {} diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 7a6bbd49e2..78425028dc 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -165,7 +165,7 @@ def formsemestre_bulletinetud_published_dict( else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) d["note"] = dict( diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index d6925d8c62..f173b56ba2 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -172,7 +172,7 @@ def make_xml_formsemestre_bulletinetud( else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt + etudid, partitions, partitions_etud_groups, nt ) doc.append( diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 80d8e02c65..d316640fc2 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -43,13 +43,14 @@ from xml.etree.ElementTree import Element import flask from flask import g, request from flask import url_for, make_response +from sqlalchemy.sql import text from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, formsemestre +from app.models import FormSemestre, Identite from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN -from app.models.groups import Partition +from app.models.groups import GroupDescr, Partition import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log, cache @@ -61,7 +62,6 @@ from app.scodoc import sco_etud from app.scodoc import sco_permissions_check from app.scodoc import sco_xml from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError -from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator @@ -413,6 +413,34 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"): return R +def get_etud_formsemestre_groups( + etud: Identite, formsemestre: FormSemestre, only_to_show=True +) -> list[GroupDescr]: + """Liste les groupes auxquels est inscrit""" + # Note: je n'ai pas réussi à cosntruire une requete SQLAlechemy avec + # la Table d'association group_membership + cursor = db.session.execute( + text( + """ + SELECT g.id + FROM group_descr g, group_membership gm, partition p + WHERE gm.etudid = :etudid + AND gm.group_id = g.id + AND g.partition_id = p.id + AND p.formsemestre_id = :formsemestre_id + AND p.partition_name is not NULL + """ + + (" and (p.show_in_lists is True) " if only_to_show else "") + + """ + ORDER BY p.numero + """ + ), + {"etudid": etud.id, "formsemestre_id": formsemestre.id}, + ) + return [GroupDescr.query.get(group_id) for group_id in cursor] + + +# Ancienne fonction: def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): """Add informations on partitions and group memberships to etud (a dict with an etudid) @@ -453,7 +481,7 @@ def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False): ) etud["partitionsgroupes"] = sep.join( [ - gr["partition_name"] + ":" + gr["group_name"] + (gr["partition_name"] or "") + ":" + gr["group_name"] for gr in infos if gr["group_name"] is not None ]