Ajout groupes et rangs/groupes aux bulletins BUT

This commit is contained in:
Emmanuel Viennet 2022-05-18 20:43:01 +02:00
parent abffc00570
commit 878ea41933
9 changed files with 179 additions and 34 deletions

View File

@ -14,10 +14,12 @@ from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.models.groups import GroupDescr
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_utils import fmt_note from app.scodoc.sco_utils import fmt_note
@ -64,8 +66,16 @@ class BulletinBUT:
# } # }
return d return d
def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict: def etud_ue_results(
"dict synthèse résultats UE" 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 res = self.res
d = { d = {
@ -81,7 +91,7 @@ class BulletinBUT:
if res.bonus_ues is not None and ue.id in res.bonus_ues if res.bonus_ues is not None and ue.id in res.bonus_ues
else fmt_note(0.0), else fmt_note(0.0),
"malus": fmt_note(res.malus[ue.id][etud.id]), "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), "ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
"saes": self.etud_ue_mod_results(etud, ue, res.saes), "saes": self.etud_ue_mod_results(etud, ue, res.saes),
} }
@ -103,6 +113,17 @@ class BulletinBUT:
"moy": fmt_note(res.etud_moy_ue[ue.id].mean()), "moy": fmt_note(res.etud_moy_ue[ue.id].mean()),
"rang": rang, "rang": rang,
"total": effectif, # nb etud avec note dans cette UE "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: else:
# ceci suppose que l'on a une seule UE bonus, # ceci suppose que l'on a une seule UE bonus,
@ -275,6 +296,9 @@ class BulletinBUT:
return d return d
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True
)
semestre_infos = { semestre_infos = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(), "date_debut": formsemestre.date_debut.isoformat(),
@ -282,7 +306,7 @@ class BulletinBUT:
"annee_universitaire": formsemestre.annee_scolaire_str(), "annee_universitaire": formsemestre.annee_scolaire_str(),
"numero": formsemestre.semestre_id, "numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb. "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"]: if self.prefs["bul_show_abs"]:
semestre_infos["absences"] = { semestre_infos["absences"] = {
@ -306,15 +330,25 @@ class BulletinBUT:
"max": fmt_note(res.etud_moy_gen.max()), "max": fmt_note(res.etud_moy_gen.max()),
} }
if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]): 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"] = { semestre_infos["rang"] = {
"value": res.etud_moy_gen_ranks[etud.id], "value": res.etud_moy_gen_ranks[etud.id],
"total": nb_inscrits, "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: else:
semestre_infos["rang"] = { semestre_infos["rang"] = {
"value": "-", "value": "-",
"total": nb_inscrits, "total": nb_inscrits,
"groupes": {},
} }
d.update( d.update(
{ {
@ -324,7 +358,10 @@ class BulletinBUT:
"saes": self.etud_mods_results(etud, res.saes, version=version), "saes": self.etud_mods_results(etud, res.saes, version=version),
"ues": { "ues": {
ue.acronyme: self.etud_ue_results( 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 for ue in res.ues
# si l'UE comporte des modules auxquels on est inscrit: # si l'UE comporte des modules auxquels on est inscrit:

View File

@ -18,7 +18,7 @@ from app.auth.models import User
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults 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 Identite
from app.models import ModuleImpl, ModuleImplInscription from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
@ -151,6 +151,7 @@ class ResultatsSemestre(ResultatsCache):
if m.module.module_type == scu.ModuleType.SAE if m.module.module_type == scu.ModuleType.SAE
] ]
# --- JURY...
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]: def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
"""Liste des UEs du semestre qui doivent être validées """Liste des UEs du semestre qui doivent être validées

View File

@ -35,7 +35,9 @@ class NotesTableCompat(ResultatsSemestre):
"malus", "malus",
"etud_moy_gen_ranks", "etud_moy_gen_ranks",
"etud_moy_gen_ranks_int", "etud_moy_gen_ranks_int",
"moy_gen_rangs_by_group",
"ue_rangs", "ue_rangs",
"ue_rangs_by_group",
) )
def __init__(self, formsemestre: FormSemestre): def __init__(self, formsemestre: FormSemestre):
@ -48,6 +50,8 @@ class NotesTableCompat(ResultatsSemestre):
self.moy_min = "NA" self.moy_min = "NA"
self.moy_max = "NA" self.moy_max = "NA"
self.moy_moy = "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.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours() self.parcours = self.formsemestre.formation.get_parcours()
@ -153,31 +157,83 @@ class NotesTableCompat(ResultatsSemestre):
def compute_rangs(self): def compute_rangs(self):
"""Calcule les classements """Calcule les classements
Moyenne générale: etud_moy_gen_ranks 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,
self.etud_moy_gen_ranks_int, self.etud_moy_gen_ranks_int,
) = moy_sem.comp_ranks_series(self.etud_moy_gen) ) = 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] moy_ue = self.etud_moy_ue[ue.id]
self.ue_rangs[ue.id] = ( self.ue_rangs[ue.id] = (
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
int(moy_ue.count()), int(moy_ue.count()),
) )
# .count() -> nb of non NaN values # .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 """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 Result: rang:str, effectif:str
""" """
if group_id is None:
rangs, effectif = self.ue_rangs[ue_id] rangs, effectif = self.ue_rangs[ue_id]
if rangs is not None: if rangs is not None:
rang = rangs[etudid] rang = rangs[etudid]
else: else:
return "", "" return "", ""
else:
rangs = self.ue_rangs_by_group[ue_id][group_id][0]
rang = rangs[etudid]
effectif = len(rangs)
return rang, effectif 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): def etud_check_conditions_ues(self, etudid):
"""Vrai si les conditions sur les UE sont remplies. """Vrai si les conditions sur les UE sont remplies.
Ne considère que les UE ayant des notes (moyenne calculée). 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é) "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]: def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
"""Liste d'informations (compat NotesTable) sur évaluations completes """Liste d'informations (compat NotesTable) sur évaluations completes
de ce module. de ce module.

View File

@ -56,11 +56,11 @@ class Identite(db.Model):
# #
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud") adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
# one-to-one relation: #
admission = db.relationship("Admission", backref="identite", lazy="dynamic") admission = db.relationship("Admission", backref="identite", lazy="dynamic")
def __repr__(self): def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>" return f"<Etud {self.id}/{self.departement.acronym} {self.nom} {self.prenom}>"
@classmethod @classmethod
def from_request(cls, etudid=None, code_nip=None): def from_request(cls, etudid=None, code_nip=None):

View File

@ -25,9 +25,11 @@ class Partition(db.Model):
partition_name = db.Column(db.String(SHORT_STR_LEN)) partition_name = db.Column(db.String(SHORT_STR_LEN))
# numero = ordre de presentation) # numero = ordre de presentation)
numero = db.Column(db.Integer) numero = db.Column(db.Integer)
# Calculer le rang ?
bul_show_rank = db.Column( bul_show_rank = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
# Montrer quand on indique les groupes de l'étudiant ?
show_in_lists = db.Column( show_in_lists = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" db.Boolean(), nullable=False, default=True, server_default="true"
) )
@ -50,6 +52,18 @@ class Partition(db.Model):
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">""" 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): class GroupDescr(db.Model):
"""Description d'un groupe d'une partition""" """Description d'un groupe d'une partition"""
@ -78,6 +92,17 @@ class GroupDescr(db.Model):
"Nom avec partition: 'TD A'" "Nom avec partition: 'TD A'"
return f"{self.partition.partition_name or ''} {self.group_name or '-'}" 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 = db.Table(
"group_membership", "group_membership",
@ -85,3 +110,11 @@ group_membership = db.Table(
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
db.UniqueConstraint("etudid", "group_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"))

View File

@ -251,7 +251,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
rang = "" rang = ""
rang_gr, ninscrits_gr, gr_name = get_etud_rangs_groups( 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(): if nt.get_moduleimpls_attente():
@ -651,7 +651,7 @@ def _ue_mod_bulletin(
def get_etud_rangs_groups( 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""" """Ramene rang et nb inscrits dans chaque partition"""
rang_gr, ninscrits_gr, gr_name = {}, {}, {} rang_gr, ninscrits_gr, gr_name = {}, {}, {}

View File

@ -165,7 +165,7 @@ def formsemestre_bulletinetud_published_dict(
else: else:
rang = str(nt.get_etud_rang(etudid)) rang = str(nt.get_etud_rang(etudid))
rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( 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( d["note"] = dict(

View File

@ -172,7 +172,7 @@ def make_xml_formsemestre_bulletinetud(
else: else:
rang = str(nt.get_etud_rang(etudid)) rang = str(nt.get_etud_rang(etudid))
rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( 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( doc.append(

View File

@ -43,13 +43,14 @@ from xml.etree.ElementTree import Element
import flask import flask
from flask import g, request from flask import g, request
from flask import url_for, make_response from flask import url_for, make_response
from sqlalchemy.sql import text
from app import db from app import db
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat 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 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.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log, cache 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_permissions_check
from app.scodoc import sco_xml from app.scodoc import sco_xml
from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
@ -413,6 +413,34 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
return R 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): def etud_add_group_infos(etud, formsemestre_id, sep=" ", only_to_show=False):
"""Add informations on partitions and group memberships to etud """Add informations on partitions and group memberships to etud
(a dict with an etudid) (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( etud["partitionsgroupes"] = sep.join(
[ [
gr["partition_name"] + ":" + gr["group_name"] (gr["partition_name"] or "") + ":" + gr["group_name"]
for gr in infos for gr in infos
if gr["group_name"] is not None if gr["group_name"] is not None
] ]