From 665451b676b7a2d1297b91b383f399c7e88426a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 5 Oct 2022 23:48:54 +0200 Subject: [PATCH] =?UTF-8?q?Remplissage=20des=20notes=20des=20=C3=A9tudiant?= =?UTF-8?q?s=20inscrits=20en=20cours=20de=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_compat.py | 1039 +++++---- app/models/evaluations.py | 8 + app/models/formsemestre.py | 28 +- app/scodoc/sco_formsemestre_status.py | 2794 +++++++++++++------------ app/scodoc/sco_saisie_notes.py | 22 +- app/views/notes.py | 6 + 6 files changed, 2047 insertions(+), 1850 deletions(-) diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index 5037b9b5..fdf6aaad 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -1,520 +1,519 @@ -############################################################################## -# ScoDoc -# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. -# See LICENSE -############################################################################## - -"""Classe résultats pour compatibilité avec le code ScoDoc 7 -""" -from functools import cached_property - -from flask import flash, g, Markup, url_for - -from app import log -from app.comp import moy_sem -from app.comp.aux_stats import StatsMoyenne -from app.comp.res_common import ResultatsSemestre -from app.comp import res_sem -from app.models import FormSemestre -from app.models import Identite -from app.models import ModuleImpl -from app.scodoc.sco_codes_parcours import UE_SPORT, DEF -from app.scodoc import sco_utils as scu - -# Pour raccorder le code des anciens codes qui attendent une NoteTable -class NotesTableCompat(ResultatsSemestre): - """Implementation partielle de NotesTable - - Les méthodes définies dans cette classe sont là - pour conserver la compatibilité abvec les codes anciens et - il n'est pas recommandé de les utiliser dans de nouveaux - développements (API malcommode et peu efficace). - """ - - _cached_attrs = ResultatsSemestre._cached_attrs + ( - "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): - super().__init__(formsemestre) - - nb_etuds = len(self.etuds) - self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues} - self.mod_rangs = None # sera surchargé en Classic, mais pas en APC - """{ modimpl_id : (rangs, effectif) }""" - 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() - self._modimpls_dict_by_ue = {} # local cache - - def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: - """Liste des étudiants inscrits - order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative) - - Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace - d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]` - """ - etuds = self.formsemestre.get_inscrits( - include_demdef=include_demdef, order=(order_by == "nom") - ) - if order_by == "moy": - etuds.sort( - key=lambda e: ( - self.etud_moy_gen_ranks_int.get(e.id, 100000), - e.sort_key, - ) - ) - return etuds - - def get_etudids(self) -> list[int]: - """(deprecated) - Liste des etudids inscrits, incluant les démissionnaires. - triée par ordre alphabetique de NOM - (à éviter: renvoie les etudids, mais est moins efficace que get_inscrits) - """ - # Note: pour avoir les inscrits non triés, - # utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ] - return [x["etudid"] for x in self.inscrlist] - - @cached_property - def sem(self) -> dict: - """le formsemestre, comme un gros et gras dict (nt.sem)""" - return self.formsemestre.get_infos_dict() - - @cached_property - def inscrlist(self) -> list[dict]: # utilisé par PE - """Liste des inscrits au semestre (avec DEM et DEF), - sous forme de dict etud, - classée dans l'ordre alphabétique de noms. - """ - etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True) - return [e.to_dict_scodoc7() for e in etuds] - - @cached_property - def stats_moy_gen(self): - """Stats (moy/min/max) sur la moyenne générale""" - return StatsMoyenne(self.etud_moy_gen) - - def get_ues_stat_dict( - self, filter_sport=False, check_apc_ects=True - ) -> list[dict]: # was get_ues() - """Liste des UEs, ordonnée par numero. - Si filter_sport, retire les UE de type SPORT. - Résultat: liste de dicts { champs UE U stats moyenne UE } - """ - ues = self.formsemestre.query_ues(with_sport=not filter_sport) - ues_dict = [] - for ue in ues: - d = ue.to_dict() - if ue.type != UE_SPORT: - moys = self.etud_moy_ue[ue.id] - else: - moys = None - d.update(StatsMoyenne(moys).to_dict()) - ues_dict.append(d) - if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"): - g.checked_apc_ects = True - if None in [ue.ects for ue in ues if ue.type != UE_SPORT]: - formation = self.formsemestre.formation - ue_sans_ects = [ - ue for ue in ues if ue.type != UE_SPORT and ue.ects is None - ] - flash( - Markup( - f"""Calcul moyenne générale impossible: ECTS des UE manquants !
- (dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])} - de la formation: {formation.get_titre_version()}) - ) - """ - ), - category="danger", - ) - return ues_dict - - def get_modimpls_dict(self, ue_id=None) -> list[dict]: - """Liste des modules pour une UE (ou toutes si ue_id==None), - triés par numéros (selon le type de formation) - """ - # cached ? - modimpls_dict = self._modimpls_dict_by_ue.get(ue_id) - if modimpls_dict: - return modimpls_dict - modimpls_dict = [] - for modimpl in self.formsemestre.modimpls_sorted: - if (ue_id is None) or (modimpl.module.ue.id == ue_id): - d = modimpl.to_dict() - # compat ScoDoc < 9.2: ajoute matières - d["mat"] = modimpl.module.matiere.to_dict() - modimpls_dict.append(d) - self._modimpls_dict_by_ue[ue_id] = modimpls_dict - return modimpls_dict - - def compute_rangs(self): - """Calcule les classements - Moyenne générale: etud_moy_gen_ranks - 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) - 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_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 - """ - if group_id is None: - rangs, effectif = self.ue_rangs[ue_id] - if rangs is not None: - rang = rangs[etudid] - else: - return "", "" - else: - 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). - (les UE sans notes ne sont pas comptées comme sous la barre) - Prend en compte les éventuelles UE capitalisées. - - Pour les parcours habituels, cela revient à vérifier que - les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes) - - Pour les parcours non standards (LP2014), cela peut être plus compliqué. - - Return: True|False, message explicatif - """ - ue_status_list = [] - for ue in self.formsemestre.query_ues(): - ue_status = self.get_etud_ue_status(etudid, ue.id) - if ue_status: - ue_status_list.append(ue_status) - return self.parcours.check_barre_ues(ue_status_list) - - def all_etuds_have_sem_decisions(self): - """True si tous les étudiants du semestre ont une décision de jury. - Ne regarde pas les décisions d'UE. - """ - for ins in self.formsemestre.inscriptions: - if ins.etat != scu.INSCRIT: - continue # skip démissionnaires - if self.get_etud_decision_sem(ins.etudid) is None: - return False - return True - - def etud_has_decision(self, etudid): - """True s'il y a une décision de jury pour cet étudiant""" - return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid) - - def get_etud_decision_ues(self, etudid: int) -> dict: - """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. - Ne tient pas compte des UE capitalisées. - { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x } - Ne renvoie aucune decision d'UE pour les défaillants - """ - if self.get_etud_etat(etudid) == DEF: - return {} - else: - validations = self.load_validations() - return validations.decisions_jury_ues.get(etudid, None) - - def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0: - """Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre. - NB: avant jury, rien d'enregistré, donc zéro ECTS. - Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decision_ues() - """ - if decisions_ues is False: - decisions_ues = self.get_etud_decision_ues(etudid) - if not decisions_ues: - return 0.0 - return sum([d.get("ects", 0.0) for d in decisions_ues.values()]) - - def get_etud_decision_sem(self, etudid: int) -> dict: - """Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu. - { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } - Si état défaillant, force le code a DEF - """ - if self.get_etud_etat(etudid) == DEF: - return { - "code": DEF, - "assidu": False, - "event_date": "", - "compense_formsemestre_id": None, - } - else: - validations = self.load_validations() - return validations.decisions_jury.get(etudid, None) - - def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str: - """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" - if not self.moyennes_matieres: - return "nd" - return ( - self.moyennes_matieres[matiere_id].get(etudid, "-") - if matiere_id in self.moyennes_matieres - else "-" - ) - - def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: - """La moyenne de l'étudiant dans le moduleimpl - En APC, il s'agira d'une moyenne indicative sans valeur. - Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) - """ - raise NotImplementedError() # virtual method - - def get_etud_moy_gen(self, etudid): # -> float | str - """Moyenne générale de cet etudiant dans ce semestre. - Prend en compte les UE capitalisées. - Si apc, moyenne indicative. - Si pas de notes: 'NA' - """ - return self.etud_moy_gen[etudid] - - def get_etud_ects_pot(self, etudid: int) -> dict: - """ - Un dict avec les champs - ects_pot : (float) nb de crédits ECTS qui seraient validés - (sous réserve de validation par le jury) - ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) - ects_total: (float) total des ECTS validables - - Les ects_pot sont les ECTS des UE au dessus de la barre (10/20 en principe), - avant le jury (donc non encore enregistrés). - """ - # was nt.get_etud_moy_infos - # XXX pour compat nt, à remplacer ultérieurement - ues = self.get_etud_ue_validables(etudid) - ects_pot = 0.0 - ects_total = 0.0 - for ue in ues: - if ue.id in self.etud_moy_ue and ue.ects is not None: - ects_total += ue.ects - if self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE: - ects_pot += ue.ects - return { - "ects_pot": ects_pot, - "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) - "ects_total": ects_total, - } - - def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: - """Liste d'informations (compat NotesTable) sur évaluations completes - de ce module. - Évaluation "complete" ssi toutes notes saisies ou en attente. - """ - modimpl = ModuleImpl.query.get(moduleimpl_id) - modimpl_results = self.modimpls_results.get(moduleimpl_id) - if not modimpl_results: - return [] # safeguard - evals_results = [] - for e in modimpl.evaluations: - if modimpl_results.evaluations_completes_dict.get(e.id, False): - d = e.to_dict() - d["heure_debut"] = e.heure_debut # datetime.time - d["heure_fin"] = e.heure_fin - d["jour"] = e.jour # datetime - d["notes"] = { - etud.id: { - "etudid": etud.id, - "value": modimpl_results.evals_notes[e.id][etud.id], - } - for etud in self.etuds - } - d["etat"] = { - "evalattente": modimpl_results.evaluations_etat[e.id].nb_attente, - } - evals_results.append(d) - elif e.id not in modimpl_results.evaluations_completes_dict: - # ne devrait pas arriver ? XXX - log( - f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?" - ) - return evals_results - - def get_evaluations_etats(self): - """[ {...evaluation et son etat...} ]""" - # TODO: à moderniser - from app.scodoc import sco_evaluations - - if not hasattr(self, "_evaluations_etats"): - self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem( - self.formsemestre.id - ) - - return self._evaluations_etats - - def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: - """Liste des états des évaluations de ce module""" - # XXX TODO à moderniser: lent, recharge des données que l'on a déjà... - return [ - e - for e in self.get_evaluations_etats() - if e["moduleimpl_id"] == moduleimpl_id - ] - - def get_moduleimpls_attente(self): - """Liste des modimpls du semestre ayant des notes en attente""" - return [ - modimpl - for modimpl in self.formsemestre.modimpls_sorted - if self.modimpls_results[modimpl.id].en_attente - ] - - def get_mod_stats(self, moduleimpl_id: int) -> dict: - """Stats sur les notes obtenues dans un modimpl - Vide en APC - """ - return { - "moy": "-", - "max": "-", - "min": "-", - "nb_notes": "-", - "nb_missing": "-", - "nb_valid_evals": "-", - } - - def get_nom_short(self, etudid): - "formatte nom d'un etud (pour table recap)" - etud = self.identdict[etudid] - return ( - (etud["nom_usuel"] or etud["nom"]).upper() - + " " - + etud["prenom"].capitalize()[:2] - + "." - ) - - @cached_property - def T(self): - return self.get_table_moyennes_triees() - - def get_table_moyennes_triees(self) -> list: - """Result: liste de tuples - moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid - """ - table_moyennes = [] - etuds_inscriptions = self.formsemestre.etuds_inscriptions - ues = self.formsemestre.query_ues(with_sport=True) # avec bonus - for etudid in etuds_inscriptions: - moy_gen = self.etud_moy_gen.get(etudid, False) - if moy_gen is False: - # pas de moyenne: démissionnaire ou def - t = ( - ["-"] - + ["0.00"] * len(self.ues) - + ["NI"] * len(self.formsemestre.modimpls_sorted) - ) - else: - moy_ues = [] - ue_is_cap = {} - for ue in ues: - ue_status = self.get_etud_ue_status(etudid, ue.id) - if ue_status: - moy_ues.append(ue_status["moy"]) - ue_is_cap[ue.id] = ue_status["is_capitalized"] - else: - moy_ues.append("?") - t = [moy_gen] + list(moy_ues) - # Moyennes modules: - for modimpl in self.formsemestre.modimpls_sorted: - if ue_is_cap.get(modimpl.module.ue.id, False): - val = "-c-" - else: - val = self.get_etud_mod_moy(modimpl.id, etudid) - t.append(val) - t.append(etudid) - table_moyennes.append(t) - # tri par moyennes décroissantes, - # en laissant les démissionnaires à la fin, par ordre alphabetique - etuds = [ins.etud for ins in etuds_inscriptions.values()] - etuds.sort(key=lambda e: e.sort_key) - self._rang_alpha = {e.id: i for i, e in enumerate(etuds)} - table_moyennes.sort(key=self._row_key) - return table_moyennes - - def _row_key(self, x): - """clé de tri par moyennes décroissantes, - en laissant les demissionnaires à la fin, par ordre alphabetique. - (moy_gen, rang_alpha) - """ - try: - moy = -float(x[0]) - except (ValueError, TypeError): - moy = 1000.0 - return (moy, self._rang_alpha[x[-1]]) - - @cached_property - def identdict(self) -> dict: - """{ etudid : etud_dict } pour tous les inscrits au semestre""" - return { - ins.etud.id: ins.etud.to_dict_scodoc7() - for ins in self.formsemestre.inscriptions - } +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Classe résultats pour compatibilité avec le code ScoDoc 7 +""" +from functools import cached_property + +from flask import flash, g, Markup, url_for + +from app import log +from app.comp import moy_sem +from app.comp.aux_stats import StatsMoyenne +from app.comp.res_common import ResultatsSemestre +from app.models import FormSemestre +from app.models import Identite +from app.models import ModuleImpl +from app.scodoc.sco_codes_parcours import UE_SPORT, DEF +from app.scodoc import sco_utils as scu + +# Pour raccorder le code des anciens codes qui attendent une NoteTable +class NotesTableCompat(ResultatsSemestre): + """Implementation partielle de NotesTable + + Les méthodes définies dans cette classe sont là + pour conserver la compatibilité abvec les codes anciens et + il n'est pas recommandé de les utiliser dans de nouveaux + développements (API malcommode et peu efficace). + """ + + _cached_attrs = ResultatsSemestre._cached_attrs + ( + "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): + super().__init__(formsemestre) + + nb_etuds = len(self.etuds) + self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues} + self.mod_rangs = None # sera surchargé en Classic, mais pas en APC + """{ modimpl_id : (rangs, effectif) }""" + 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() + self._modimpls_dict_by_ue = {} # local cache + + def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]: + """Liste des étudiants inscrits + order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative) + + Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace + d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]` + """ + etuds = self.formsemestre.get_inscrits( + include_demdef=include_demdef, order=(order_by == "nom") + ) + if order_by == "moy": + etuds.sort( + key=lambda e: ( + self.etud_moy_gen_ranks_int.get(e.id, 100000), + e.sort_key, + ) + ) + return etuds + + def get_etudids(self) -> list[int]: + """(deprecated) + Liste des etudids inscrits, incluant les démissionnaires. + triée par ordre alphabetique de NOM + (à éviter: renvoie les etudids, mais est moins efficace que get_inscrits) + """ + # Note: pour avoir les inscrits non triés, + # utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ] + return [x["etudid"] for x in self.inscrlist] + + @cached_property + def sem(self) -> dict: + """le formsemestre, comme un gros et gras dict (nt.sem)""" + return self.formsemestre.get_infos_dict() + + @cached_property + def inscrlist(self) -> list[dict]: # utilisé par PE + """Liste des inscrits au semestre (avec DEM et DEF), + sous forme de dict etud, + classée dans l'ordre alphabétique de noms. + """ + etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True) + return [e.to_dict_scodoc7() for e in etuds] + + @cached_property + def stats_moy_gen(self): + """Stats (moy/min/max) sur la moyenne générale""" + return StatsMoyenne(self.etud_moy_gen) + + def get_ues_stat_dict( + self, filter_sport=False, check_apc_ects=True + ) -> list[dict]: # was get_ues() + """Liste des UEs, ordonnée par numero. + Si filter_sport, retire les UE de type SPORT. + Résultat: liste de dicts { champs UE U stats moyenne UE } + """ + ues = self.formsemestre.query_ues(with_sport=not filter_sport) + ues_dict = [] + for ue in ues: + d = ue.to_dict() + if ue.type != UE_SPORT: + moys = self.etud_moy_ue[ue.id] + else: + moys = None + d.update(StatsMoyenne(moys).to_dict()) + ues_dict.append(d) + if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"): + g.checked_apc_ects = True + if None in [ue.ects for ue in ues if ue.type != UE_SPORT]: + formation = self.formsemestre.formation + ue_sans_ects = [ + ue for ue in ues if ue.type != UE_SPORT and ue.ects is None + ] + flash( + Markup( + f"""Calcul moyenne générale impossible: ECTS des UE manquants !
+ (dans {' ,'.join([ue.acronyme for ue in ue_sans_ects])} + de la formation: {formation.get_titre_version()}) + ) + """ + ), + category="danger", + ) + return ues_dict + + def get_modimpls_dict(self, ue_id=None) -> list[dict]: + """Liste des modules pour une UE (ou toutes si ue_id==None), + triés par numéros (selon le type de formation) + """ + # cached ? + modimpls_dict = self._modimpls_dict_by_ue.get(ue_id) + if modimpls_dict: + return modimpls_dict + modimpls_dict = [] + for modimpl in self.formsemestre.modimpls_sorted: + if (ue_id is None) or (modimpl.module.ue.id == ue_id): + d = modimpl.to_dict() + # compat ScoDoc < 9.2: ajoute matières + d["mat"] = modimpl.module.matiere.to_dict() + modimpls_dict.append(d) + self._modimpls_dict_by_ue[ue_id] = modimpls_dict + return modimpls_dict + + def compute_rangs(self): + """Calcule les classements + Moyenne générale: etud_moy_gen_ranks + 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) + 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_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 + """ + if group_id is None: + rangs, effectif = self.ue_rangs[ue_id] + if rangs is not None: + rang = rangs[etudid] + else: + return "", "" + else: + 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). + (les UE sans notes ne sont pas comptées comme sous la barre) + Prend en compte les éventuelles UE capitalisées. + + Pour les parcours habituels, cela revient à vérifier que + les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes) + + Pour les parcours non standards (LP2014), cela peut être plus compliqué. + + Return: True|False, message explicatif + """ + ue_status_list = [] + for ue in self.formsemestre.query_ues(): + ue_status = self.get_etud_ue_status(etudid, ue.id) + if ue_status: + ue_status_list.append(ue_status) + return self.parcours.check_barre_ues(ue_status_list) + + def all_etuds_have_sem_decisions(self): + """True si tous les étudiants du semestre ont une décision de jury. + Ne regarde pas les décisions d'UE. + """ + for ins in self.formsemestre.inscriptions: + if ins.etat != scu.INSCRIT: + continue # skip démissionnaires + if self.get_etud_decision_sem(ins.etudid) is None: + return False + return True + + def etud_has_decision(self, etudid): + """True s'il y a une décision de jury pour cet étudiant""" + return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid) + + def get_etud_decision_ues(self, etudid: int) -> dict: + """Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu. + Ne tient pas compte des UE capitalisées. + { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : "d/m/y", 'ects' : x } + Ne renvoie aucune decision d'UE pour les défaillants + """ + if self.get_etud_etat(etudid) == DEF: + return {} + else: + validations = self.load_validations() + return validations.decisions_jury_ues.get(etudid, None) + + def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0: + """Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre. + NB: avant jury, rien d'enregistré, donc zéro ECTS. + Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decision_ues() + """ + if decisions_ues is False: + decisions_ues = self.get_etud_decision_ues(etudid) + if not decisions_ues: + return 0.0 + return sum([d.get("ects", 0.0) for d in decisions_ues.values()]) + + def get_etud_decision_sem(self, etudid: int) -> dict: + """Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu. + { 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id } + Si état défaillant, force le code a DEF + """ + if self.get_etud_etat(etudid) == DEF: + return { + "code": DEF, + "assidu": False, + "event_date": "", + "compense_formsemestre_id": None, + } + else: + validations = self.load_validations() + return validations.decisions_jury.get(etudid, None) + + def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str: + """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" + if not self.moyennes_matieres: + return "nd" + return ( + self.moyennes_matieres[matiere_id].get(etudid, "-") + if matiere_id in self.moyennes_matieres + else "-" + ) + + def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: + """La moyenne de l'étudiant dans le moduleimpl + En APC, il s'agira d'une moyenne indicative sans valeur. + Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) + """ + raise NotImplementedError() # virtual method + + def get_etud_moy_gen(self, etudid): # -> float | str + """Moyenne générale de cet etudiant dans ce semestre. + Prend en compte les UE capitalisées. + Si apc, moyenne indicative. + Si pas de notes: 'NA' + """ + return self.etud_moy_gen[etudid] + + def get_etud_ects_pot(self, etudid: int) -> dict: + """ + Un dict avec les champs + ects_pot : (float) nb de crédits ECTS qui seraient validés + (sous réserve de validation par le jury) + ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives) + ects_total: (float) total des ECTS validables + + Les ects_pot sont les ECTS des UE au dessus de la barre (10/20 en principe), + avant le jury (donc non encore enregistrés). + """ + # was nt.get_etud_moy_infos + # XXX pour compat nt, à remplacer ultérieurement + ues = self.get_etud_ue_validables(etudid) + ects_pot = 0.0 + ects_total = 0.0 + for ue in ues: + if ue.id in self.etud_moy_ue and ue.ects is not None: + ects_total += ue.ects + if self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE: + ects_pot += ue.ects + return { + "ects_pot": ects_pot, + "ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé) + "ects_total": ects_total, + } + + def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]: + """Liste d'informations (compat NotesTable) sur évaluations completes + de ce module. + Évaluation "complete" ssi toutes notes saisies ou en attente. + """ + modimpl = ModuleImpl.query.get(moduleimpl_id) + modimpl_results = self.modimpls_results.get(moduleimpl_id) + if not modimpl_results: + return [] # safeguard + evals_results = [] + for e in modimpl.evaluations: + if modimpl_results.evaluations_completes_dict.get(e.id, False): + d = e.to_dict() + d["heure_debut"] = e.heure_debut # datetime.time + d["heure_fin"] = e.heure_fin + d["jour"] = e.jour # datetime + d["notes"] = { + etud.id: { + "etudid": etud.id, + "value": modimpl_results.evals_notes[e.id][etud.id], + } + for etud in self.etuds + } + d["etat"] = { + "evalattente": modimpl_results.evaluations_etat[e.id].nb_attente, + } + evals_results.append(d) + elif e.id not in modimpl_results.evaluations_completes_dict: + # ne devrait pas arriver ? XXX + log( + f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?" + ) + return evals_results + + def get_evaluations_etats(self): + """[ {...evaluation et son etat...} ]""" + # TODO: à moderniser + from app.scodoc import sco_evaluations + + if not hasattr(self, "_evaluations_etats"): + self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem( + self.formsemestre.id + ) + + return self._evaluations_etats + + def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]: + """Liste des états des évaluations de ce module""" + # XXX TODO à moderniser: lent, recharge des données que l'on a déjà... + return [ + e + for e in self.get_evaluations_etats() + if e["moduleimpl_id"] == moduleimpl_id + ] + + def get_moduleimpls_attente(self): + """Liste des modimpls du semestre ayant des notes en attente""" + return [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if self.modimpls_results[modimpl.id].en_attente + ] + + def get_mod_stats(self, moduleimpl_id: int) -> dict: + """Stats sur les notes obtenues dans un modimpl + Vide en APC + """ + return { + "moy": "-", + "max": "-", + "min": "-", + "nb_notes": "-", + "nb_missing": "-", + "nb_valid_evals": "-", + } + + def get_nom_short(self, etudid): + "formatte nom d'un etud (pour table recap)" + etud = self.identdict[etudid] + return ( + (etud["nom_usuel"] or etud["nom"]).upper() + + " " + + etud["prenom"].capitalize()[:2] + + "." + ) + + @cached_property + def T(self): + return self.get_table_moyennes_triees() + + def get_table_moyennes_triees(self) -> list: + """Result: liste de tuples + moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid + """ + table_moyennes = [] + etuds_inscriptions = self.formsemestre.etuds_inscriptions + ues = self.formsemestre.query_ues(with_sport=True) # avec bonus + for etudid in etuds_inscriptions: + moy_gen = self.etud_moy_gen.get(etudid, False) + if moy_gen is False: + # pas de moyenne: démissionnaire ou def + t = ( + ["-"] + + ["0.00"] * len(self.ues) + + ["NI"] * len(self.formsemestre.modimpls_sorted) + ) + else: + moy_ues = [] + ue_is_cap = {} + for ue in ues: + ue_status = self.get_etud_ue_status(etudid, ue.id) + if ue_status: + moy_ues.append(ue_status["moy"]) + ue_is_cap[ue.id] = ue_status["is_capitalized"] + else: + moy_ues.append("?") + t = [moy_gen] + list(moy_ues) + # Moyennes modules: + for modimpl in self.formsemestre.modimpls_sorted: + if ue_is_cap.get(modimpl.module.ue.id, False): + val = "-c-" + else: + val = self.get_etud_mod_moy(modimpl.id, etudid) + t.append(val) + t.append(etudid) + table_moyennes.append(t) + # tri par moyennes décroissantes, + # en laissant les démissionnaires à la fin, par ordre alphabetique + etuds = [ins.etud for ins in etuds_inscriptions.values()] + etuds.sort(key=lambda e: e.sort_key) + self._rang_alpha = {e.id: i for i, e in enumerate(etuds)} + table_moyennes.sort(key=self._row_key) + return table_moyennes + + def _row_key(self, x): + """clé de tri par moyennes décroissantes, + en laissant les demissionnaires à la fin, par ordre alphabetique. + (moy_gen, rang_alpha) + """ + try: + moy = -float(x[0]) + except (ValueError, TypeError): + moy = 1000.0 + return (moy, self._rang_alpha[x[-1]]) + + @cached_property + def identdict(self) -> dict: + """{ etudid : etud_dict } pour tous les inscrits au semestre""" + return { + ins.etud.id: ins.etud.to_dict_scodoc7() + for ins in self.formsemestre.inscriptions + } diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 805744af..04ed7dce 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -5,7 +5,9 @@ import datetime from app import db +from app.models.etudiants import Identite from app.models.moduleimpls import ModuleImpl +from app.models.notes import NotesNotes from app.models.ues import UniteEns from app.scodoc.sco_exceptions import ScoValueError @@ -176,6 +178,12 @@ class Evaluation(db.Model): ] ) + def get_etud_note(self, etud: Identite) -> NotesNotes: + """La note de l'étudiant, ou None si pas noté. + (nb: pas de cache, lent, ne pas utiliser pour des calculs) + """ + return NotesNotes.query.filter_by(etudid=etud.id, evaluation_id=self.id).first() + class EvaluationUEPoids(db.Model): """Poids des évaluations (BUT) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 5742da15..f376ef74 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -27,13 +27,14 @@ from app.models.but_refcomp import ( ApcReferentielCompetences, ) from app.models.groups import GroupDescr, Partition +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu from app.models.but_refcomp import parcours_formsemestre from app.models.etudiants import Identite from app.models.formations import Formation from app.models.modules import Module -from app.models.moduleimpls import ModuleImpl +from app.models.moduleimpls import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation @@ -690,6 +691,31 @@ class FormSemestre(db.Model): ) return "\n".join(H) + def etud_set_all_missing_notes(self, etud: Identite, value=None) -> int: + """Met toutes les notes manquantes de cet étudiant dans ce semestre + (ie dans toutes les évaluations des modules auxquels il est inscrit et n'a pas de note) + à la valeur donnée par value, qui est en général "ATT", "ABS", "EXC". + """ + from app.scodoc import sco_saisie_notes + + inscriptions = ( + ModuleImplInscription.query.filter_by(etudid=etud.id) + .join(ModuleImpl) + .filter_by(formsemestre_id=self.id) + ) + nb_recorded = 0 + for inscription in inscriptions: + for evaluation in inscription.modimpl.evaluations: + if evaluation.get_etud_note(etud) is None: + if not sco_saisie_notes.do_evaluation_set_etud_note( + evaluation, etud, value + ): + raise ScoValueError( + "erreur lors de l'enregistrement de la note" + ) + nb_recorded += 1 + return nb_recorded + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index ad64acd0..24f3d6e1 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1,1328 +1,1466 @@ -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -############################################################################## -# -# Gestion scolarite IUT -# -# Copyright (c) 1999 - 2022 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 -# -############################################################################## - -"""Tableau de bord semestre -""" - -import datetime -from flask import current_app -from flask import g -from flask import request -from flask import render_template, url_for -from flask_login import current_user - -from app import log -from app.comp import res_sem -from app.comp.res_compat import NotesTableCompat -from app.models import Evaluation, Module -from app.models.formsemestre import FormSemestre -import app.scodoc.sco_utils as scu -from app.scodoc.sco_utils import ModuleType -from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_exceptions import ( - ScoValueError, - ScoInvalidDateError, - ScoInvalidIdType, -) -from app.scodoc import html_sco_header -from app.scodoc import htmlutils -from app.scodoc import sco_abs -from app.scodoc import sco_archives -from app.scodoc import sco_bulletins -from app.scodoc import sco_codes_parcours -from app.scodoc import sco_compute_moy -from app.scodoc import sco_edit_ue -from app.scodoc import sco_evaluations -from app.scodoc import sco_evaluation_db -from app.scodoc import sco_formations -from app.scodoc import sco_formsemestre -from app.scodoc import sco_formsemestre_inscriptions -from app.scodoc import sco_groups -from app.scodoc import sco_moduleimpl -from app.scodoc import sco_permissions_check -from app.scodoc import sco_preferences -from app.scodoc import sco_users -from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html -import sco_version - - -def _build_menu_stats(formsemestre_id): - "Définition du menu 'Statistiques'" - return [ - { - "title": "Statistiques...", - "endpoint": "notes.formsemestre_report_counts", - "args": {"formsemestre_id": formsemestre_id}, - }, - { - "title": "Suivi de cohortes", - "endpoint": "notes.formsemestre_suivi_cohorte", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - { - "title": "Graphe des parcours", - "endpoint": "notes.formsemestre_graph_parcours", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - { - "title": "Codes des parcours", - "endpoint": "notes.formsemestre_suivi_parcours", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - { - "title": "Lycées d'origine", - "endpoint": "notes.formsemestre_etuds_lycees", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - { - "title": 'Table "poursuite"', - "endpoint": "notes.formsemestre_poursuite_report", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - { - "title": "Documents Avis Poursuite Etudes (xp)", - "endpoint": "notes.pe_view_sem_recap", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, # current_app.config["TESTING"] or current_app.config["DEBUG"], - }, - { - "title": 'Table "débouchés"', - "endpoint": "notes.report_debouche_date", - "enabled": True, - }, - { - "title": "Estimation du coût de la formation", - "endpoint": "notes.formsemestre_estim_cost", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - { - "title": "Indicateurs de suivi annuel BUT", - "endpoint": "notes.formsemestre_but_indicateurs", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - }, - ] - - -def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: - """HTML to render menubar""" - formsemestre_id = formsemestre.id - if formsemestre.etat: - change_lock_msg = "Verrouiller" - else: - change_lock_msg = "Déverrouiller" - - formation = formsemestre.formation - - # L'utilisateur est-il resp. du semestre ? - is_responsable = current_user.id in (u.id for u in formsemestre.responsables) - # A le droit de changer le semestre (déverrouiller, préférences bul., ...): - has_perm_change_sem = current_user.has_permission(Permission.ScoImplement) or ( - formsemestre.resp_can_edit and is_responsable - ) - # Peut modifier le semestre (si n'est pas verrouillé): - can_modify_sem = has_perm_change_sem and formsemestre.etat - - menu_semestre = [ - { - "title": "Tableau de bord", - "endpoint": "notes.formsemestre_status", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "Tableau de bord du semestre", - }, - { - "title": f"Voir la formation {formation.acronyme} (v{formation.version})", - "endpoint": "notes.ue_table", - "args": { - "formation_id": formation.id, - "semestre_idx": formsemestre.semestre_id, - }, - "enabled": True, - "helpmsg": "Tableau de bord du semestre", - }, - { - "title": "Modifier le semestre", - "endpoint": "notes.formsemestre_editwithmodules", - "args": { - "formation_id": formation.id, - "formsemestre_id": formsemestre_id, - }, - "enabled": can_modify_sem, - "helpmsg": "Modifie le contenu du semestre (modules)", - }, - { - "title": "Préférences du semestre", - "endpoint": "scolar.formsemestre_edit_preferences", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": can_modify_sem, - "helpmsg": "Préférences du semestre", - }, - { - "title": "Réglages bulletins", - "endpoint": "notes.formsemestre_edit_options", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": has_perm_change_sem, - "helpmsg": "Change les options", - }, - { - "title": change_lock_msg, - "endpoint": "notes.formsemestre_change_lock", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": has_perm_change_sem, - "helpmsg": "", - }, - { - "title": "Description du semestre", - "endpoint": "notes.formsemestre_description", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "", - }, - { - "title": "Vérifier absences aux évaluations", - "endpoint": "notes.formsemestre_check_absences_html", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "", - }, - { - "title": "Lister tous les enseignants", - "endpoint": "notes.formsemestre_enseignants_list", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "", - }, - { - "title": "Cloner ce semestre", - "endpoint": "notes.formsemestre_clone", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoImplement), - "helpmsg": "", - }, - { - "title": "Associer à une nouvelle version du programme", - "endpoint": "notes.formsemestre_associate_new_version", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoChangeFormation) - and formsemestre.etat, - "helpmsg": "", - }, - { - "title": "Supprimer ce semestre", - "endpoint": "notes.formsemestre_delete", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoImplement), - "helpmsg": "", - }, - ] - # debug : - if current_app.config["ENV"] == "development": - menu_semestre.append( - { - "title": "Vérifier l'intégrité", - "endpoint": "notes.check_sem_integrity", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - } - ) - - menu_inscriptions = [ - { - "title": "Voir les inscriptions aux modules", - "endpoint": "notes.moduleimpl_inscriptions_stats", - "args": {"formsemestre_id": formsemestre_id}, - } - ] - menu_inscriptions += [ - { - "title": "Passage des étudiants depuis d'autres semestres", - "endpoint": "notes.formsemestre_inscr_passage", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoEtudInscrit) - and formsemestre.etat, - }, - { - "title": "Synchroniser avec étape Apogée", - "endpoint": "notes.formsemestre_synchro_etuds", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoView) - and sco_preferences.get_preference("portal_url") - and formsemestre.etat, - }, - { - "title": "Inscrire un étudiant", - "endpoint": "notes.formsemestre_inscription_with_modules_etud", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoEtudInscrit) - and formsemestre.etat, - }, - { - "title": "Importer des étudiants dans ce semestre (table Excel)", - "endpoint": "scolar.form_students_import_excel", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoEtudInscrit) - and formsemestre.etat, - }, - { - "title": "Import/export des données admission", - "endpoint": "scolar.form_students_import_infos_admissions", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoView), - }, - { - "title": "Resynchroniser données identité", - "endpoint": "scolar.formsemestre_import_etud_admission", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_user.has_permission(Permission.ScoEtudChangeAdr) - and sco_preferences.get_preference("portal_url"), - }, - { - "title": "Exporter table des étudiants", - "endpoint": "scolar.groups_view", - "args": { - "format": "allxls", - "group_ids": sco_groups.get_default_group( - formsemestre_id, fix_if_missing=True - ), - }, - }, - { - "title": "Vérifier inscriptions multiples", - "endpoint": "notes.formsemestre_inscrits_ailleurs", - "args": {"formsemestre_id": formsemestre_id}, - }, - ] - - menu_groupes = [ - { - "title": "Listes, photos, feuilles...", - "endpoint": "scolar.groups_view", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "Accès aux listes des groupes d'étudiants", - }, - { - "title": "Créer/modifier les partitions...", - "endpoint": "scolar.edit_partition_form", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_groups.sco_permissions_check.can_change_groups( - formsemestre_id - ), - }, - ] - # 1 item / partition: - partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False) - submenu = [] - enabled = ( - sco_groups.sco_permissions_check.can_change_groups(formsemestre_id) - and partitions - ) - for partition in partitions: - submenu.append( - { - "title": str(partition["partition_name"]), - "endpoint": "scolar.affect_groups", - "args": {"partition_id": partition["partition_id"]}, - "enabled": enabled, - } - ) - menu_groupes.append( - {"title": "Modifier les groupes", "submenu": submenu, "enabled": enabled} - ) - menu_groupes.append( - { - "title": "Expérimental: éditeur de partitions", - "endpoint": "scolar.partition_editor", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_groups.sco_permissions_check.can_change_groups( - formsemestre_id - ), - "helpmsg": "Une spécialité de Mulhouse", - }, - ) - - menu_notes = [ - { - "title": "Tableau des moyennes (et liens bulletins)", - "endpoint": "notes.formsemestre_recapcomplet", - "args": {"formsemestre_id": formsemestre_id}, - }, - { - "title": "État des évaluations", - "endpoint": "notes.evaluations_recap", - "args": {"formsemestre_id": formsemestre_id}, - }, - { - "title": "Saisie des notes", - "endpoint": "notes.formsemestre_status", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": True, - "helpmsg": "Tableau de bord du semestre", - }, - { - "title": "Classeur PDF des bulletins", - "endpoint": "notes.formsemestre_bulletins_pdf_choice", - "args": {"formsemestre_id": formsemestre_id}, - "helpmsg": "PDF regroupant tous les bulletins", - }, - { - "title": "Envoyer à chaque étudiant son bulletin par e-mail", - "endpoint": "notes.formsemestre_bulletins_mailetuds_choice", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_bulletins.can_send_bulletin_by_mail(formsemestre_id), - }, - { - "title": "Calendrier des évaluations", - "endpoint": "notes.formsemestre_evaluations_cal", - "args": {"formsemestre_id": formsemestre_id}, - }, - { - "title": "Lister toutes les saisies de notes", - "endpoint": "notes.formsemestre_list_saisies_notes", - "args": {"formsemestre_id": formsemestre_id}, - }, - ] - menu_jury = [ - { - "title": "Voir les décisions du jury", - "endpoint": "notes.formsemestre_pvjury", - "args": {"formsemestre_id": formsemestre_id}, - }, - { - "title": "Générer feuille préparation Jury", - "endpoint": "notes.feuille_preparation_jury", - "args": {"formsemestre_id": formsemestre_id}, - }, - { - "title": "Saisie des décisions du jury", - "endpoint": "notes.formsemestre_saisie_jury", - "args": { - "formsemestre_id": formsemestre_id, - }, - "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), - }, - { - "title": "Éditer les PV et archiver les résultats", - "endpoint": "notes.formsemestre_archive", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_permissions_check.can_edit_pv(formsemestre_id), - }, - { - "title": "Documents archivés", - "endpoint": "notes.formsemestre_list_archives", - "args": {"formsemestre_id": formsemestre_id}, - "enabled": sco_archives.PVArchive.list_obj_archives(formsemestre_id), - }, - ] - - menu_stats = _build_menu_stats(formsemestre_id) - H = [ - '", - ] - return "\n".join(H) - - -def retreive_formsemestre_from_request() -> int: - """Cherche si on a de quoi déduire le semestre affiché à partir des - arguments de la requête: - formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id - Returns None si pas défini. - """ - if request.method == "GET": - args = request.args - elif request.method == "POST": - args = request.form - else: - return None - formsemestre_id = None - # Search formsemestre - group_ids = args.get("group_ids", []) - if "formsemestre_id" in args: - formsemestre_id = args["formsemestre_id"] - elif "moduleimpl_id" in args and args["moduleimpl_id"]: - modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"]) - if not modimpl: - return None # suppressed ? - modimpl = modimpl[0] - formsemestre_id = modimpl["formsemestre_id"] - elif "evaluation_id" in args: - E = sco_evaluation_db.do_evaluation_list( - {"evaluation_id": args["evaluation_id"]} - ) - if not E: - return None # evaluation suppressed ? - E = E[0] - modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] - formsemestre_id = modimpl["formsemestre_id"] - elif "group_id" in args: - group = sco_groups.get_group(args["group_id"]) - formsemestre_id = group["formsemestre_id"] - elif group_ids: - if group_ids: - if isinstance(group_ids, str): - group_id = group_ids - else: - # prend le semestre du 1er groupe de la liste: - group_id = group_ids[0] - group = sco_groups.get_group(group_id) - formsemestre_id = group["formsemestre_id"] - elif "partition_id" in args: - partition = sco_groups.get_partition(args["partition_id"]) - formsemestre_id = partition["formsemestre_id"] - - if not formsemestre_id: - return None # no current formsemestre - - return int(formsemestre_id) - - -# Element HTML decrivant un semestre (barre de menu et infos) -def formsemestre_page_title(formsemestre_id=None): - """Element HTML decrivant un semestre (barre de menu et infos) - Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group) - """ - formsemestre_id = ( - formsemestre_id - if formsemestre_id is not None - else retreive_formsemestre_from_request() - ) - # - if not formsemestre_id: - return "" - try: - formsemestre_id = int(formsemestre_id) - except ValueError: - log(f"formsemestre_id: invalid type {formsemestre_id:r}") - return "" - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - - h = render_template( - "formsemestre_page_title.html", - formsemestre=formsemestre, - scu=scu, - sem_menu_bar=formsemestre_status_menubar(formsemestre), - ) - - return h - - -def fill_formsemestre(sem): - """Add some useful fields to help display formsemestres""" - sem["notes_url"] = scu.NotesURL() - formsemestre_id = sem["formsemestre_id"] - if not sem["etat"]: - sem[ - "locklink" - ] = f"""{scu.icontag("lock_img", border="0", title="Semestre verrouillé")}""" - else: - sem["locklink"] = "" - if sco_preferences.get_preference("bul_display_publication", formsemestre_id): - if sem["bul_hide_xml"]: - eyeicon = scu.icontag("hide_img", border="0", title="Bulletins NON publiés") - else: - eyeicon = scu.icontag("eye_img", border="0", title="Bulletins publiés") - sem[ - "eyelink" - ] = f"""{eyeicon}""" - else: - sem["eyelink"] = "" - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] - sem["formation"] = F - parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) - if sem["semestre_id"] != -1: - sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}""" - else: - sem["num_sem"] = "" # formation sans semestres - if sem["modalite"]: - sem["modalitestr"] = f""" en {sem["modalite"]}""" - else: - sem["modalitestr"] = "" - - sem["etape_apo_str"] = "Code étape Apogée: " + ( - sco_formsemestre.formsemestre_etape_apo_str(sem) or "Pas de code étape" - ) - - inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - args={"formsemestre_id": formsemestre_id} - ) - sem["nbinscrits"] = len(inscrits) - uresps = [ - sco_users.user_info(responsable_id) for responsable_id in sem["responsables"] - ] - sem["resp"] = ", ".join([u["prenomnom"] for u in uresps]) - sem["nomcomplet"] = ", ".join([u["nomcomplet"] for u in uresps]) - - -# Description du semestre sous forme de table exportable -def formsemestre_description_table( - formsemestre_id: int, with_evals=False, with_parcours=False -): - """Description du semestre sous forme de table exportable - Liste des modules et de leurs coefficients - """ - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) - F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[ - 0 - ] - parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) - # --- Colonnes à proposer: - columns_ids = ["UE", "Code", "Module"] - if with_parcours: - columns_ids += ["parcours"] - if not formsemestre.formation.is_apc(): - columns_ids += ["Coef."] - ues = [] # liste des UE, seulement en APC pour les coefs - else: - ues = formsemestre.query_ues().all() - columns_ids += [f"ue_{ue.id}" for ue in ues] - if sco_preferences.get_preference("bul_show_ects", formsemestre_id): - columns_ids += ["ects"] - columns_ids += ["Inscrits", "Responsable", "Enseignants"] - if with_evals: - columns_ids += [ - "jour", - "description", - "coefficient", - "evalcomplete_str", - "publish_incomplete_str", - ] - - titles = {title: title for title in columns_ids} - titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) - titles["ects"] = "ECTS" - titles["jour"] = "Evaluation" - titles["description"] = "" - titles["coefficient"] = "Coef. éval." - titles["evalcomplete_str"] = "Complète" - titles["parcours"] = "Parcours" - titles["publish_incomplete_str"] = "Toujours Utilisée" - title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}" - - R = [] - sum_coef = 0 - sum_ects = 0 - last_ue_id = None - for modimpl in formsemestre.modimpls_sorted: - # Ligne UE avec ECTS: - ue = modimpl.module.ue - if ue.id != last_ue_id: - last_ue_id = ue.id - if ue.ects is None: - ects_str = "-" - else: - sum_ects += ue.ects - ects_str = ue.ects - ue_info = { - "UE": ue.acronyme, - "ects": ects_str, - "Module": ue.titre, - "_css_row_class": "table_row_ue", - "_UE_td_attrs": f'style="background-color: {ue.color} !important;"' - if ue.color - else "", - } - if use_ue_coefs: - ue_info["Coef."] = ue.coefficient - ue_info["Coef._class"] = "ue_coef" - R.append(ue_info) - - mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( - moduleimpl_id=modimpl.id - ) - enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants) - - l = { - "UE": modimpl.module.ue.acronyme, - "_UE_td_attrs": ue_info["_UE_td_attrs"], - "Code": modimpl.module.code or "", - "Module": modimpl.module.abbrev or modimpl.module.titre, - "_Module_class": "scotext", - "Inscrits": len(mod_inscrits), - "Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"], - "_Responsable_class": "scotext", - "Enseignants": enseignants, - "_Enseignants_class": "scotext", - "Coef.": modimpl.module.coefficient, - # 'ECTS' : M['module']['ects'], - # Lien sur titre -> module - "_Module_target": url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ), - "_Code_target": url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ), - } - if modimpl.module.coefficient is not None: - sum_coef += modimpl.module.coefficient - coef_dict = modimpl.module.get_ue_coef_dict() - for ue in ues: - l[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or "" - if with_parcours: - l["parcours"] = ", ".join( - sorted([pa.code for pa in modimpl.module.parcours]) - ) - - R.append(l) - - if with_evals: - # Ajoute lignes pour evaluations - evals = nt.get_mod_evaluation_etat_list(modimpl.id) - evals.reverse() # ordre chronologique - # Ajoute etat: - for e in evals: - e["UE"] = l["UE"] - e["_UE_td_attrs"] = l["_UE_td_attrs"] - e["Code"] = l["Code"] - e["_css_row_class"] = "evaluation" - e["Module"] = "éval." - # Cosmetic: conversions pour affichage - if e["etat"]["evalcomplete"]: - e["evalcomplete_str"] = "Oui" - e["_evalcomplete_str_td_attrs"] = 'style="color: green;"' - else: - e["evalcomplete_str"] = "Non" - e["_evalcomplete_str_td_attrs"] = 'style="color: red;"' - - if e["publish_incomplete"]: - e["publish_incomplete_str"] = "Oui" - e["_publish_incomplete_str_td_attrs"] = 'style="color: green;"' - else: - e["publish_incomplete_str"] = "Non" - e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"' - # Poids vers UEs (en APC) - evaluation: Evaluation = Evaluation.query.get(e["evaluation_id"]) - for ue_id, poids in evaluation.get_ue_poids_dict().items(): - e[f"ue_{ue_id}"] = poids or "" - e[f"_ue_{ue_id}_class"] = "poids" - e[f"_ue_{ue_id}_help"] = "poids vers l'UE" - - R += evals - - sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} - R.append(sums) - - return GenTable( - columns_ids=columns_ids, - rows=R, - titles=titles, - origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", - caption=title, - html_caption=title, - html_class="table_leftalign formsemestre_description", - base_url="%s?formsemestre_id=%s&with_evals=%s" - % (request.base_url, formsemestre_id, with_evals), - page_title=title, - html_title=html_sco_header.html_sem_header( - "Description du semestre", with_page_header=False - ), - pdf_title=title, - preferences=sco_preferences.SemPreferences(formsemestre_id), - ) - - -def formsemestre_description( - formsemestre_id, format="html", with_evals=False, with_parcours=False -): - """Description du semestre sous forme de table exportable - Liste des modules et de leurs coefficients - """ - with_evals = int(with_evals) - tab = formsemestre_description_table( - formsemestre_id, with_evals=with_evals, with_parcours=with_parcours - ) - tab.html_before_table = f""" -
- - indiquer les évaluations - indiquer les parcours BUT - """ - - return tab.make_page(format=format) - - -# genere liste html pour accès aux groupes de ce semestre -def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) - destination = url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - ) - # - H = [] - # pas de menu absences si pas autorise: - if with_absences and not current_user.has_permission(Permission.ScoAbsChange): - with_absences = False - - # - H.append( - f"""

Listes de {formsemestre.titre} - ({formsemestre.mois_debut()} - {formsemestre.mois_fin()})

""" - ) - - weekday = datetime.datetime.today().weekday() - try: - if with_absences: - first_monday = sco_abs.ddmmyyyy( - formsemestre.date_debut.strftime("%d/%m/%Y") - ).prev_monday() - form_abs_tmpl = f""" - - absences - - - - - - - - - - saisie par semaine -
- """ - else: - form_abs_tmpl = "" - except ScoInvalidDateError: # dates incorrectes dans semestres ? - form_abs_tmpl = "" - # - H.append('
') - # Genere liste pour chaque partition (categorie de groupes) - for partition in sco_groups.get_partitions_list(formsemestre.id): - if not partition["partition_name"]: - H.append("

Tous les étudiants

") - else: - H.append("

Groupes de %(partition_name)s

" % partition) - groups = sco_groups.get_partition_groups(partition) - if groups: - H.append("") - for group in groups: - n_members = len(sco_groups.get_group_members(group["group_id"])) - group["url_etat"] = url_for( - "absences.EtatAbsencesGr", - group_ids=group["group_id"], - debut=formsemestre.date_debut.strftime("%d/%m/%Y"), - fin=formsemestre.date_fin.strftime("%d/%m/%Y"), - scodoc_dept=g.scodoc_dept, - ) - if group["group_name"]: - group["label"] = "groupe %(group_name)s" % group - else: - group["label"] = "liste" - H.append( - f""" - - - - """ - ) - - if with_absences: - H.append(form_abs_tmpl % group) - - H.append("") - H.append("
") - else: - H.append('

Aucun groupe dans cette partition') - if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id): - H.append( - f""" (créer)""" - ) - H.append("

") - if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id): - H.append( - f"""

Ajouter une partition

""" - ) - - H.append("
") - return "\n".join(H) - - -def html_expr_diagnostic(diagnostics): - """Affiche messages d'erreur des formules utilisateurs""" - H = [] - H.append('
Erreur dans des formules utilisateurs:
") - return "".join(H) - - -def formsemestre_status_head(formsemestre_id=None, page_title=None): - """En-tête HTML des pages "semestre" """ - sem = FormSemestre.query.get(formsemestre_id) - if not sem: - raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)") - formation = sem.formation - parcours = formation.get_parcours() - - page_title = page_title or "Modules de " - - H = [ - html_sco_header.html_sem_header( - page_title, with_page_header=False, with_h2=False - ), - f""" - ") - if sem.parcours: - H.append( - f""" - - - - """ - ) - - evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) - H.append( - '") - if evals["attente"]: - H.append( - """""" - ) - H.append("
Formation: - {formation.titre} - """, - ] - if sem.semestre_id >= 0: - H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id)) - if sem.modalite: - H.append(f" en {sem.modalite}") - if sem.etapes: - H.append( - f"""   (étape { - sem.etapes_apo_str() or "-" - })""" - ) - H.append("
Parcours: {', '.join(parcours.code for parcours in sem.parcours)}
Évaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' - % evals - ) - if evals["last_modif"]: - H.append( - " (dernière note saisie le %s)" - % evals["last_modif"].strftime("%d/%m/%Y à %Hh%M") - ) - H.append("
-Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur indicative. -
") - sem_warning = "" - if sem.bul_hide_xml: - sem_warning += "Bulletins non publiés sur le portail. " - if sem.block_moyennes: - sem_warning += "Calcul des moyennes bloqué !" - if sem_warning: - H.append('

' + sem_warning + "

") - if sem.semestre_id >= 0 and not sem.est_sur_une_annee(): - H.append( - '

Attention: ce semestre couvre plusieurs années scolaires !

' - ) - - return "".join(H) - - -def formsemestre_status(formsemestre_id=None): - """Tableau de bord semestre HTML""" - # porté du DTML - if formsemestre_id is not None and not isinstance(formsemestre_id, int): - raise ScoInvalidIdType( - "formsemestre_bulletinetud: formsemestre_id must be an integer !" - ) - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - modimpls = sco_moduleimpl.moduleimpl_withmodule_list( - formsemestre_id=formsemestre_id - ) - nt = res_sem.load_formsemestre_results(formsemestre) - - # Construit la liste de tous les enseignants de ce semestre: - mails_enseignants = set(u.email for u in formsemestre.responsables) - for modimpl in modimpls: - mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"]) - mails_enseignants |= set( - [sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]] - ) - - can_edit = formsemestre.can_be_edited_by(current_user) - use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) - - H = [ - html_sco_header.sco_header( - page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}" - ), - '
', - formsemestre_status_head( - formsemestre_id=formsemestre_id, page_title="Tableau de bord" - ), - """

Tableau de bord: - cliquez sur un module pour saisir des notes -

""", - ] - - if nt.expr_diagnostics: - H.append(html_expr_diagnostic(nt.expr_diagnostics)) - - if nt.parcours.APC_SAE: - # BUT: tableau ressources puis SAE - ressources = [ - m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE - ] - saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE] - autres = [ - m - for m in modimpls - if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE) - ] - H += [ - f""" -
- {_TABLEAU_MODULES_HEAD} - - - Ressources - - - {formsemestre_tableau_modules( - ressources, nt, formsemestre_id, can_edit=can_edit, show_ues=False - )} - - - SAÉs - - """, - formsemestre_tableau_modules( - saes, nt, formsemestre_id, can_edit=can_edit, show_ues=False - ), - ] - if autres: - H += [ - """ - - Autres modules - """, - formsemestre_tableau_modules( - autres, nt, formsemestre_id, can_edit=can_edit, show_ues=False - ), - ] - H += [_TABLEAU_MODULES_FOOT, "
"] - else: - # formations classiques: groupe par UE - H += [ - "

", - _TABLEAU_MODULES_HEAD, - formsemestre_tableau_modules( - modimpls, - nt, - formsemestre_id, - can_edit=can_edit, - use_ue_coefs=use_ue_coefs, - ), - _TABLEAU_MODULES_FOOT, - "

", - ] - - if use_ue_coefs and not formsemestre.formation.is_apc(): - H.append( - """ -

utilise les coefficients d'UE pour calculer la moyenne générale.

- """ - ) - # --- LISTE DES ETUDIANTS - H += [ - '
', - _make_listes_sem(formsemestre), - "
", - ] - # --- Lien mail enseignants: - adrlist = list(mails_enseignants - {None, ""}) - if adrlist: - H.append( - '

Courrier aux %d enseignants du semestre

' - % (",".join(adrlist), len(adrlist)) - ) - return "".join(H) + html_sco_header.sco_footer() - - -_TABLEAU_MODULES_HEAD = """ - - - - - - - - - -""" -_TABLEAU_MODULES_FOOT = """
CodeModuleInscritsResponsableCoefs.Évaluations
""" - - -def formsemestre_tableau_modules( - modimpls: list[dict], - nt, - formsemestre_id: int, - can_edit=True, - show_ues=True, - use_ue_coefs=False, -) -> str: - "Lignes table HTML avec modules du semestre" - H = [] - prev_ue_id = None - for modimpl in modimpls: - mod: Module = Module.query.get(modimpl["module_id"]) - mod_descr = "Module " + (mod.titre or "") - if mod.is_apc(): - coef_descr = ", ".join( - [f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()] - ) - if coef_descr: - mod_descr += " Coefs: " + coef_descr - else: - mod_descr += " (pas de coefficients) " - else: - mod_descr += ", coef. " + str(mod.coefficient) - mod_ens = sco_users.user_info(modimpl["responsable_id"])["nomcomplet"] - if modimpl["ens"]: - mod_ens += " (resp.), " + ", ".join( - [sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]] - ) - mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( - moduleimpl_id=modimpl["moduleimpl_id"] - ) - - ue = modimpl["ue"] - if show_ues and (prev_ue_id != ue["ue_id"]): - prev_ue_id = ue["ue_id"] - titre = ue["titre"] - if use_ue_coefs: - titre += " (coef. %s)" % (ue["coefficient"] or 0.0) - H.append( - f""" - {ue["acronyme"]} - {titre} - """ - ) - - expr = sco_compute_moy.get_ue_expression( - formsemestre_id, ue["ue_id"], html_quote=True - ) - if expr: - H.append( - f""" {expr} - formule inutilisée en 9.2: supprimer""" - ) - - H.append("") - - if modimpl["ue"]["type"] != sco_codes_parcours.UE_STANDARD: - fontorange = " fontorange" # style css additionnel - else: - fontorange = "" - - etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"]) - # if nt.parcours.APC_SAE: - # tbd style si module non conforme - if ( - etat["nb_evals_completes"] > 0 - and etat["nb_evals_en_cours"] == 0 - and etat["nb_evals_vides"] == 0 - ): - H.append(f'') - else: - H.append(f'') - - H.append( - f"""{mod.code}""" - ) - H.append( - f"""{mod.abbrev or mod.titre or ""} - - {len(mod_inscrits)} - - { sco_users.user_info(modimpl["responsable_id"])["prenomnom"] } - - - """ - ) - if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): - coefs = mod.ue_coefs_list() - H.append(f'') - for coef in coefs: - if coef[1] > 0: - H.append( - f"""""" - ) - else: - H.append("""""") - H.append("") - H.append("") - if mod.module_type in ( - None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs - ModuleType.STANDARD, - ModuleType.RESSOURCE, - ModuleType.SAE, - ): - H.append('') - nb_evals = ( - etat["nb_evals_completes"] - + etat["nb_evals_en_cours"] - + etat["nb_evals_vides"] - ) - if nb_evals != 0: - H.append( - '%s prévues, %s ok' - % (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"]) - ) - if etat["nb_evals_en_cours"] > 0: - H.append( - ', %s en cours' - % (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"]) - ) - if etat["attente"]: - H.append( - ' [en attente]' - % modimpl["moduleimpl_id"] - ) - elif mod.module_type == ModuleType.MALUS: - nb_malus_notes = sum( - [ - e["etat"]["nb_notes"] - for e in nt.get_mod_evaluation_etat_list(modimpl["moduleimpl_id"]) - ] - ) - H.append( - """ - malus (%d notes) - """ - % (modimpl["moduleimpl_id"], nb_malus_notes) - ) - else: - raise ValueError(f"Invalid module_type {mod.module_type}") # a bug - - H.append("") - return "\n".join(H) +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2022 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 +# +############################################################################## + +"""Tableau de bord semestre +""" + +import datetime + +from flask import current_app +from flask import g +from flask import request +from flask import flash, redirect, render_template, url_for +from flask_login import current_user +import pandas as pd +from app import log +from app.comp import res_sem +from app.comp.res_common import ResultatsSemestre +from app.comp.res_compat import NotesTableCompat +from app.models import Evaluation, Module +from app.models.etudiants import Identite +from app.models.formsemestre import FormSemestre +import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType +from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_exceptions import ( + ScoValueError, + ScoInvalidDateError, + ScoInvalidIdType, +) +from app.scodoc import html_sco_header +from app.scodoc import htmlutils +from app.scodoc import sco_abs +from app.scodoc import sco_archives +from app.scodoc import sco_bulletins +from app.scodoc import sco_codes_parcours +from app.scodoc import sco_compute_moy +from app.scodoc import sco_edit_ue +from app.scodoc import sco_evaluations +from app.scodoc import sco_evaluation_db +from app.scodoc import sco_formations +from app.scodoc import sco_formsemestre +from app.scodoc import sco_formsemestre_inscriptions +from app.scodoc import sco_groups +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_permissions_check +from app.scodoc import sco_preferences +from app.scodoc import sco_users +from app.scodoc.gen_tables import GenTable +from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html +import sco_version + + +def _build_menu_stats(formsemestre_id): + "Définition du menu 'Statistiques'" + return [ + { + "title": "Statistiques...", + "endpoint": "notes.formsemestre_report_counts", + "args": {"formsemestre_id": formsemestre_id}, + }, + { + "title": "Suivi de cohortes", + "endpoint": "notes.formsemestre_suivi_cohorte", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + { + "title": "Graphe des parcours", + "endpoint": "notes.formsemestre_graph_parcours", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + { + "title": "Codes des parcours", + "endpoint": "notes.formsemestre_suivi_parcours", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + { + "title": "Lycées d'origine", + "endpoint": "notes.formsemestre_etuds_lycees", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + { + "title": 'Table "poursuite"', + "endpoint": "notes.formsemestre_poursuite_report", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + { + "title": "Documents Avis Poursuite Etudes (xp)", + "endpoint": "notes.pe_view_sem_recap", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, # current_app.config["TESTING"] or current_app.config["DEBUG"], + }, + { + "title": 'Table "débouchés"', + "endpoint": "notes.report_debouche_date", + "enabled": True, + }, + { + "title": "Estimation du coût de la formation", + "endpoint": "notes.formsemestre_estim_cost", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + { + "title": "Indicateurs de suivi annuel BUT", + "endpoint": "notes.formsemestre_but_indicateurs", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + }, + ] + + +def formsemestre_status_menubar(formsemestre: FormSemestre) -> str: + """HTML to render menubar""" + formsemestre_id = formsemestre.id + if formsemestre.etat: + change_lock_msg = "Verrouiller" + else: + change_lock_msg = "Déverrouiller" + + formation = formsemestre.formation + + # L'utilisateur est-il resp. du semestre ? + is_responsable = current_user.id in (u.id for u in formsemestre.responsables) + # A le droit de changer le semestre (déverrouiller, préférences bul., ...): + has_perm_change_sem = current_user.has_permission(Permission.ScoImplement) or ( + formsemestre.resp_can_edit and is_responsable + ) + # Peut modifier le semestre (si n'est pas verrouillé): + can_modify_sem = has_perm_change_sem and formsemestre.etat + + menu_semestre = [ + { + "title": "Tableau de bord", + "endpoint": "notes.formsemestre_status", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + "helpmsg": "Tableau de bord du semestre", + }, + { + "title": f"Voir la formation {formation.acronyme} (v{formation.version})", + "endpoint": "notes.ue_table", + "args": { + "formation_id": formation.id, + "semestre_idx": formsemestre.semestre_id, + }, + "enabled": True, + "helpmsg": "Tableau de bord du semestre", + }, + { + "title": "Modifier le semestre", + "endpoint": "notes.formsemestre_editwithmodules", + "args": { + "formation_id": formation.id, + "formsemestre_id": formsemestre_id, + }, + "enabled": can_modify_sem, + "helpmsg": "Modifie le contenu du semestre (modules)", + }, + { + "title": "Préférences du semestre", + "endpoint": "scolar.formsemestre_edit_preferences", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": can_modify_sem, + "helpmsg": "Préférences du semestre", + }, + { + "title": "Réglages bulletins", + "endpoint": "notes.formsemestre_edit_options", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": has_perm_change_sem, + "helpmsg": "Change les options", + }, + { + "title": change_lock_msg, + "endpoint": "notes.formsemestre_change_lock", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": has_perm_change_sem, + "helpmsg": "", + }, + { + "title": "Description du semestre", + "endpoint": "notes.formsemestre_description", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + "helpmsg": "", + }, + { + "title": "Vérifier absences aux évaluations", + "endpoint": "notes.formsemestre_check_absences_html", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + "helpmsg": "", + }, + { + "title": "Lister tous les enseignants", + "endpoint": "notes.formsemestre_enseignants_list", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + "helpmsg": "", + }, + { + "title": "Cloner ce semestre", + "endpoint": "notes.formsemestre_clone", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoImplement), + "helpmsg": "", + }, + { + "title": "Associer à une nouvelle version du programme", + "endpoint": "notes.formsemestre_associate_new_version", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoChangeFormation) + and formsemestre.etat, + "helpmsg": "", + }, + { + "title": "Supprimer ce semestre", + "endpoint": "notes.formsemestre_delete", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoImplement), + "helpmsg": "", + }, + ] + # debug : + if current_app.config["ENV"] == "development": + menu_semestre.append( + { + "title": "Vérifier l'intégrité", + "endpoint": "notes.check_sem_integrity", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + } + ) + + menu_inscriptions = [ + { + "title": "Voir les inscriptions aux modules", + "endpoint": "notes.moduleimpl_inscriptions_stats", + "args": {"formsemestre_id": formsemestre_id}, + } + ] + menu_inscriptions += [ + { + "title": "Passage des étudiants depuis d'autres semestres", + "endpoint": "notes.formsemestre_inscr_passage", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoEtudInscrit) + and formsemestre.etat, + }, + { + "title": "Synchroniser avec étape Apogée", + "endpoint": "notes.formsemestre_synchro_etuds", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoView) + and sco_preferences.get_preference("portal_url") + and formsemestre.etat, + }, + { + "title": "Inscrire un étudiant", + "endpoint": "notes.formsemestre_inscription_with_modules_etud", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoEtudInscrit) + and formsemestre.etat, + }, + { + "title": "Importer des étudiants dans ce semestre (table Excel)", + "endpoint": "scolar.form_students_import_excel", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoEtudInscrit) + and formsemestre.etat, + }, + { + "title": "Import/export des données admission", + "endpoint": "scolar.form_students_import_infos_admissions", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoView), + }, + { + "title": "Resynchroniser données identité", + "endpoint": "scolar.formsemestre_import_etud_admission", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": current_user.has_permission(Permission.ScoEtudChangeAdr) + and sco_preferences.get_preference("portal_url"), + }, + { + "title": "Exporter table des étudiants", + "endpoint": "scolar.groups_view", + "args": { + "format": "allxls", + "group_ids": sco_groups.get_default_group( + formsemestre_id, fix_if_missing=True + ), + }, + }, + { + "title": "Vérifier inscriptions multiples", + "endpoint": "notes.formsemestre_inscrits_ailleurs", + "args": {"formsemestre_id": formsemestre_id}, + }, + ] + + menu_groupes = [ + { + "title": "Listes, photos, feuilles...", + "endpoint": "scolar.groups_view", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + "helpmsg": "Accès aux listes des groupes d'étudiants", + }, + { + "title": "Créer/modifier les partitions...", + "endpoint": "scolar.edit_partition_form", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": sco_groups.sco_permissions_check.can_change_groups( + formsemestre_id + ), + }, + ] + # 1 item / partition: + partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False) + submenu = [] + enabled = ( + sco_groups.sco_permissions_check.can_change_groups(formsemestre_id) + and partitions + ) + for partition in partitions: + submenu.append( + { + "title": str(partition["partition_name"]), + "endpoint": "scolar.affect_groups", + "args": {"partition_id": partition["partition_id"]}, + "enabled": enabled, + } + ) + menu_groupes.append( + {"title": "Modifier les groupes", "submenu": submenu, "enabled": enabled} + ) + menu_groupes.append( + { + "title": "Expérimental: éditeur de partitions", + "endpoint": "scolar.partition_editor", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": sco_groups.sco_permissions_check.can_change_groups( + formsemestre_id + ), + "helpmsg": "Une spécialité de Mulhouse", + }, + ) + + menu_notes = [ + { + "title": "Tableau des moyennes (et liens bulletins)", + "endpoint": "notes.formsemestre_recapcomplet", + "args": {"formsemestre_id": formsemestre_id}, + }, + { + "title": "État des évaluations", + "endpoint": "notes.evaluations_recap", + "args": {"formsemestre_id": formsemestre_id}, + }, + { + "title": "Saisie des notes", + "endpoint": "notes.formsemestre_status", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": True, + "helpmsg": "Tableau de bord du semestre", + }, + { + "title": "Classeur PDF des bulletins", + "endpoint": "notes.formsemestre_bulletins_pdf_choice", + "args": {"formsemestre_id": formsemestre_id}, + "helpmsg": "PDF regroupant tous les bulletins", + }, + { + "title": "Envoyer à chaque étudiant son bulletin par e-mail", + "endpoint": "notes.formsemestre_bulletins_mailetuds_choice", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": sco_bulletins.can_send_bulletin_by_mail(formsemestre_id), + }, + { + "title": "Calendrier des évaluations", + "endpoint": "notes.formsemestre_evaluations_cal", + "args": {"formsemestre_id": formsemestre_id}, + }, + { + "title": "Lister toutes les saisies de notes", + "endpoint": "notes.formsemestre_list_saisies_notes", + "args": {"formsemestre_id": formsemestre_id}, + }, + ] + menu_jury = [ + { + "title": "Voir les décisions du jury", + "endpoint": "notes.formsemestre_pvjury", + "args": {"formsemestre_id": formsemestre_id}, + }, + { + "title": "Générer feuille préparation Jury", + "endpoint": "notes.feuille_preparation_jury", + "args": {"formsemestre_id": formsemestre_id}, + }, + { + "title": "Saisie des décisions du jury", + "endpoint": "notes.formsemestre_saisie_jury", + "args": { + "formsemestre_id": formsemestre_id, + }, + "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), + }, + { + "title": "Éditer les PV et archiver les résultats", + "endpoint": "notes.formsemestre_archive", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": sco_permissions_check.can_edit_pv(formsemestre_id), + }, + { + "title": "Documents archivés", + "endpoint": "notes.formsemestre_list_archives", + "args": {"formsemestre_id": formsemestre_id}, + "enabled": sco_archives.PVArchive.list_obj_archives(formsemestre_id), + }, + ] + + menu_stats = _build_menu_stats(formsemestre_id) + H = [ + '", + ] + return "\n".join(H) + + +def retreive_formsemestre_from_request() -> int: + """Cherche si on a de quoi déduire le semestre affiché à partir des + arguments de la requête: + formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id + Returns None si pas défini. + """ + if request.method == "GET": + args = request.args + elif request.method == "POST": + args = request.form + else: + return None + formsemestre_id = None + # Search formsemestre + group_ids = args.get("group_ids", []) + if "formsemestre_id" in args: + formsemestre_id = args["formsemestre_id"] + elif "moduleimpl_id" in args and args["moduleimpl_id"]: + modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"]) + if not modimpl: + return None # suppressed ? + modimpl = modimpl[0] + formsemestre_id = modimpl["formsemestre_id"] + elif "evaluation_id" in args: + E = sco_evaluation_db.do_evaluation_list( + {"evaluation_id": args["evaluation_id"]} + ) + if not E: + return None # evaluation suppressed ? + E = E[0] + modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] + formsemestre_id = modimpl["formsemestre_id"] + elif "group_id" in args: + group = sco_groups.get_group(args["group_id"]) + formsemestre_id = group["formsemestre_id"] + elif group_ids: + if group_ids: + if isinstance(group_ids, str): + group_id = group_ids + else: + # prend le semestre du 1er groupe de la liste: + group_id = group_ids[0] + group = sco_groups.get_group(group_id) + formsemestre_id = group["formsemestre_id"] + elif "partition_id" in args: + partition = sco_groups.get_partition(args["partition_id"]) + formsemestre_id = partition["formsemestre_id"] + + if not formsemestre_id: + return None # no current formsemestre + + return int(formsemestre_id) + + +# Element HTML decrivant un semestre (barre de menu et infos) +def formsemestre_page_title(formsemestre_id=None): + """Element HTML decrivant un semestre (barre de menu et infos) + Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group) + """ + formsemestre_id = ( + formsemestre_id + if formsemestre_id is not None + else retreive_formsemestre_from_request() + ) + # + if not formsemestre_id: + return "" + try: + formsemestre_id = int(formsemestre_id) + except ValueError: + log(f"formsemestre_id: invalid type {formsemestre_id:r}") + return "" + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + + h = render_template( + "formsemestre_page_title.html", + formsemestre=formsemestre, + scu=scu, + sem_menu_bar=formsemestre_status_menubar(formsemestre), + ) + + return h + + +def fill_formsemestre(sem): + """Add some useful fields to help display formsemestres""" + sem["notes_url"] = scu.NotesURL() + formsemestre_id = sem["formsemestre_id"] + if not sem["etat"]: + sem[ + "locklink" + ] = f"""{scu.icontag("lock_img", border="0", title="Semestre verrouillé")}""" + else: + sem["locklink"] = "" + if sco_preferences.get_preference("bul_display_publication", formsemestre_id): + if sem["bul_hide_xml"]: + eyeicon = scu.icontag("hide_img", border="0", title="Bulletins NON publiés") + else: + eyeicon = scu.icontag("eye_img", border="0", title="Bulletins publiés") + sem[ + "eyelink" + ] = f"""{eyeicon}""" + else: + sem["eyelink"] = "" + F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] + sem["formation"] = F + parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) + if sem["semestre_id"] != -1: + sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}""" + else: + sem["num_sem"] = "" # formation sans semestres + if sem["modalite"]: + sem["modalitestr"] = f""" en {sem["modalite"]}""" + else: + sem["modalitestr"] = "" + + sem["etape_apo_str"] = "Code étape Apogée: " + ( + sco_formsemestre.formsemestre_etape_apo_str(sem) or "Pas de code étape" + ) + + inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( + args={"formsemestre_id": formsemestre_id} + ) + sem["nbinscrits"] = len(inscrits) + uresps = [ + sco_users.user_info(responsable_id) for responsable_id in sem["responsables"] + ] + sem["resp"] = ", ".join([u["prenomnom"] for u in uresps]) + sem["nomcomplet"] = ", ".join([u["nomcomplet"] for u in uresps]) + + +# Description du semestre sous forme de table exportable +def formsemestre_description_table( + formsemestre_id: int, with_evals=False, with_parcours=False +): + """Description du semestre sous forme de table exportable + Liste des modules et de leurs coefficients + """ + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) + F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[ + 0 + ] + parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) + # --- Colonnes à proposer: + columns_ids = ["UE", "Code", "Module"] + if with_parcours: + columns_ids += ["parcours"] + if not formsemestre.formation.is_apc(): + columns_ids += ["Coef."] + ues = [] # liste des UE, seulement en APC pour les coefs + else: + ues = formsemestre.query_ues().all() + columns_ids += [f"ue_{ue.id}" for ue in ues] + if sco_preferences.get_preference("bul_show_ects", formsemestre_id): + columns_ids += ["ects"] + columns_ids += ["Inscrits", "Responsable", "Enseignants"] + if with_evals: + columns_ids += [ + "jour", + "description", + "coefficient", + "evalcomplete_str", + "publish_incomplete_str", + ] + + titles = {title: title for title in columns_ids} + titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) + titles["ects"] = "ECTS" + titles["jour"] = "Evaluation" + titles["description"] = "" + titles["coefficient"] = "Coef. éval." + titles["evalcomplete_str"] = "Complète" + titles["parcours"] = "Parcours" + titles["publish_incomplete_str"] = "Toujours Utilisée" + title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}" + + R = [] + sum_coef = 0 + sum_ects = 0 + last_ue_id = None + for modimpl in formsemestre.modimpls_sorted: + # Ligne UE avec ECTS: + ue = modimpl.module.ue + if ue.id != last_ue_id: + last_ue_id = ue.id + if ue.ects is None: + ects_str = "-" + else: + sum_ects += ue.ects + ects_str = ue.ects + ue_info = { + "UE": ue.acronyme, + "ects": ects_str, + "Module": ue.titre, + "_css_row_class": "table_row_ue", + "_UE_td_attrs": f'style="background-color: {ue.color} !important;"' + if ue.color + else "", + } + if use_ue_coefs: + ue_info["Coef."] = ue.coefficient + ue_info["Coef._class"] = "ue_coef" + R.append(ue_info) + + mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( + moduleimpl_id=modimpl.id + ) + enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants) + + l = { + "UE": modimpl.module.ue.acronyme, + "_UE_td_attrs": ue_info["_UE_td_attrs"], + "Code": modimpl.module.code or "", + "Module": modimpl.module.abbrev or modimpl.module.titre, + "_Module_class": "scotext", + "Inscrits": len(mod_inscrits), + "Responsable": sco_users.user_info(modimpl.responsable_id)["nomprenom"], + "_Responsable_class": "scotext", + "Enseignants": enseignants, + "_Enseignants_class": "scotext", + "Coef.": modimpl.module.coefficient, + # 'ECTS' : M['module']['ects'], + # Lien sur titre -> module + "_Module_target": url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ), + "_Code_target": url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ), + } + if modimpl.module.coefficient is not None: + sum_coef += modimpl.module.coefficient + coef_dict = modimpl.module.get_ue_coef_dict() + for ue in ues: + l[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or "" + if with_parcours: + l["parcours"] = ", ".join( + sorted([pa.code for pa in modimpl.module.parcours]) + ) + + R.append(l) + + if with_evals: + # Ajoute lignes pour evaluations + evals = nt.get_mod_evaluation_etat_list(modimpl.id) + evals.reverse() # ordre chronologique + # Ajoute etat: + for e in evals: + e["UE"] = l["UE"] + e["_UE_td_attrs"] = l["_UE_td_attrs"] + e["Code"] = l["Code"] + e["_css_row_class"] = "evaluation" + e["Module"] = "éval." + # Cosmetic: conversions pour affichage + if e["etat"]["evalcomplete"]: + e["evalcomplete_str"] = "Oui" + e["_evalcomplete_str_td_attrs"] = 'style="color: green;"' + else: + e["evalcomplete_str"] = "Non" + e["_evalcomplete_str_td_attrs"] = 'style="color: red;"' + + if e["publish_incomplete"]: + e["publish_incomplete_str"] = "Oui" + e["_publish_incomplete_str_td_attrs"] = 'style="color: green;"' + else: + e["publish_incomplete_str"] = "Non" + e["_publish_incomplete_str_td_attrs"] = 'style="color: red;"' + # Poids vers UEs (en APC) + evaluation: Evaluation = Evaluation.query.get(e["evaluation_id"]) + for ue_id, poids in evaluation.get_ue_poids_dict().items(): + e[f"ue_{ue_id}"] = poids or "" + e[f"_ue_{ue_id}_class"] = "poids" + e[f"_ue_{ue_id}_help"] = "poids vers l'UE" + + R += evals + + sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} + R.append(sums) + + return GenTable( + columns_ids=columns_ids, + rows=R, + titles=titles, + origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", + caption=title, + html_caption=title, + html_class="table_leftalign formsemestre_description", + base_url="%s?formsemestre_id=%s&with_evals=%s" + % (request.base_url, formsemestre_id, with_evals), + page_title=title, + html_title=html_sco_header.html_sem_header( + "Description du semestre", with_page_header=False + ), + pdf_title=title, + preferences=sco_preferences.SemPreferences(formsemestre_id), + ) + + +def formsemestre_description( + formsemestre_id, format="html", with_evals=False, with_parcours=False +): + """Description du semestre sous forme de table exportable + Liste des modules et de leurs coefficients + """ + with_evals = int(with_evals) + tab = formsemestre_description_table( + formsemestre_id, with_evals=with_evals, with_parcours=with_parcours + ) + tab.html_before_table = f""" +
+ + indiquer les évaluations + indiquer les parcours BUT + """ + + return tab.make_page(format=format) + + +# genere liste html pour accès aux groupes de ce semestre +def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): + # construit l'URL "destination" + # (a laquelle on revient apres saisie absences) + destination = url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + # + H = [] + # pas de menu absences si pas autorise: + if with_absences and not current_user.has_permission(Permission.ScoAbsChange): + with_absences = False + + # + H.append( + f"""

Listes de {formsemestre.titre} + ({formsemestre.mois_debut()} - {formsemestre.mois_fin()})

""" + ) + + weekday = datetime.datetime.today().weekday() + try: + if with_absences: + first_monday = sco_abs.ddmmyyyy( + formsemestre.date_debut.strftime("%d/%m/%Y") + ).prev_monday() + form_abs_tmpl = f""" + + absences + + + + + + + + + + saisie par semaine +
+ """ + else: + form_abs_tmpl = "" + except ScoInvalidDateError: # dates incorrectes dans semestres ? + form_abs_tmpl = "" + # + H.append('
') + # Genere liste pour chaque partition (categorie de groupes) + for partition in sco_groups.get_partitions_list(formsemestre.id): + if not partition["partition_name"]: + H.append("

Tous les étudiants

") + else: + H.append("

Groupes de %(partition_name)s

" % partition) + groups = sco_groups.get_partition_groups(partition) + if groups: + H.append("") + for group in groups: + n_members = len(sco_groups.get_group_members(group["group_id"])) + group["url_etat"] = url_for( + "absences.EtatAbsencesGr", + group_ids=group["group_id"], + debut=formsemestre.date_debut.strftime("%d/%m/%Y"), + fin=formsemestre.date_fin.strftime("%d/%m/%Y"), + scodoc_dept=g.scodoc_dept, + ) + if group["group_name"]: + group["label"] = "groupe %(group_name)s" % group + else: + group["label"] = "liste" + H.append( + f""" + + + + """ + ) + + if with_absences: + H.append(form_abs_tmpl % group) + + H.append("") + H.append("
") + else: + H.append('

Aucun groupe dans cette partition') + if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id): + H.append( + f""" (créer)""" + ) + H.append("

") + if sco_groups.sco_permissions_check.can_change_groups(formsemestre.id): + H.append( + f"""

Ajouter une partition

""" + ) + + H.append("
") + return "\n".join(H) + + +def html_expr_diagnostic(diagnostics): + """Affiche messages d'erreur des formules utilisateurs""" + H = [] + H.append('
Erreur dans des formules utilisateurs:
    ') + last_id, last_msg = None, None + for diag in diagnostics: + if "moduleimpl_id" in diag: + mod = sco_moduleimpl.moduleimpl_withmodule_list( + moduleimpl_id=diag["moduleimpl_id"] + )[0] + H.append( + '
  • module %s: %s
  • ' + % ( + diag["moduleimpl_id"], + mod["module"]["abbrev"] or mod["module"]["code"] or "?", + diag["msg"], + ) + ) + else: + if diag["ue_id"] != last_id or diag["msg"] != last_msg: + ue = sco_edit_ue.ue_list({"ue_id": diag["ue_id"]})[0] + H.append( + '
  • UE "%s": %s
  • ' + % (ue["acronyme"] or ue["titre"] or "?", diag["msg"]) + ) + last_id, last_msg = diag["ue_id"], diag["msg"] + + H.append("
") + return "".join(H) + + +def formsemestre_status_head(formsemestre_id=None, page_title=None): + """En-tête HTML des pages "semestre" """ + sem = FormSemestre.query.get(formsemestre_id) + if not sem: + raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)") + formation = sem.formation + parcours = formation.get_parcours() + + page_title = page_title or "Modules de " + + H = [ + html_sco_header.html_sem_header( + page_title, with_page_header=False, with_h2=False + ), + f""" + ") + if sem.parcours: + H.append( + f""" + + + + """ + ) + + evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) + H.append( + '") + if evals["attente"]: + H.append( + """""" + ) + H.append("
Formation: + {formation.titre} + """, + ] + if sem.semestre_id >= 0: + H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id)) + if sem.modalite: + H.append(f" en {sem.modalite}") + if sem.etapes: + H.append( + f"""   (étape { + sem.etapes_apo_str() or "-" + })""" + ) + H.append("
Parcours: {', '.join(parcours.code for parcours in sem.parcours)}
Évaluations: %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides' + % evals + ) + if evals["last_modif"]: + H.append( + " (dernière note saisie le %s)" + % evals["last_modif"].strftime("%d/%m/%Y à %Hh%M") + ) + H.append("
+Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur indicative. +
") + sem_warning = "" + if sem.bul_hide_xml: + sem_warning += "Bulletins non publiés sur le portail. " + if sem.block_moyennes: + sem_warning += "Calcul des moyennes bloqué !" + if sem_warning: + H.append('

' + sem_warning + "

") + if sem.semestre_id >= 0 and not sem.est_sur_une_annee(): + H.append( + '

Attention: ce semestre couvre plusieurs années scolaires !

' + ) + + return "".join(H) + + +def formsemestre_status(formsemestre_id=None): + """Tableau de bord semestre HTML""" + # porté du DTML + if formsemestre_id is not None and not isinstance(formsemestre_id, int): + raise ScoInvalidIdType( + "formsemestre_bulletinetud: formsemestre_id must be an integer !" + ) + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + modimpls = sco_moduleimpl.moduleimpl_withmodule_list( + formsemestre_id=formsemestre_id + ) + nt = res_sem.load_formsemestre_results(formsemestre) + + # Construit la liste de tous les enseignants de ce semestre: + mails_enseignants = set(u.email for u in formsemestre.responsables) + for modimpl in modimpls: + mails_enseignants.add(sco_users.user_info(modimpl["responsable_id"])["email"]) + mails_enseignants |= set( + [sco_users.user_info(m["ens_id"])["email"] for m in modimpl["ens"]] + ) + + can_edit = formsemestre.can_be_edited_by(current_user) + use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) + + H = [ + html_sco_header.sco_header( + page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}" + ), + '
', + formsemestre_status_head( + formsemestre_id=formsemestre_id, page_title="Tableau de bord" + ), + formsemestre_warning_etuds_sans_note(formsemestre, nt), + """

Tableau de bord: + cliquez sur un module pour saisir des notes +

""", + ] + + if nt.expr_diagnostics: + H.append(html_expr_diagnostic(nt.expr_diagnostics)) + + if nt.parcours.APC_SAE: + # BUT: tableau ressources puis SAE + ressources = [ + m for m in modimpls if m["module"]["module_type"] == ModuleType.RESSOURCE + ] + saes = [m for m in modimpls if m["module"]["module_type"] == ModuleType.SAE] + autres = [ + m + for m in modimpls + if m["module"]["module_type"] not in (ModuleType.RESSOURCE, ModuleType.SAE) + ] + H += [ + f""" +
+ {_TABLEAU_MODULES_HEAD} + + + Ressources + + + {formsemestre_tableau_modules( + ressources, nt, formsemestre_id, can_edit=can_edit, show_ues=False + )} + + + SAÉs + + """, + formsemestre_tableau_modules( + saes, nt, formsemestre_id, can_edit=can_edit, show_ues=False + ), + ] + if autres: + H += [ + """ + + Autres modules + """, + formsemestre_tableau_modules( + autres, nt, formsemestre_id, can_edit=can_edit, show_ues=False + ), + ] + H += [_TABLEAU_MODULES_FOOT, "
"] + else: + # formations classiques: groupe par UE + H += [ + "

", + _TABLEAU_MODULES_HEAD, + formsemestre_tableau_modules( + modimpls, + nt, + formsemestre_id, + can_edit=can_edit, + use_ue_coefs=use_ue_coefs, + ), + _TABLEAU_MODULES_FOOT, + "

", + ] + + if use_ue_coefs and not formsemestre.formation.is_apc(): + H.append( + """ +

utilise les coefficients d'UE pour calculer la moyenne générale.

+ """ + ) + # --- LISTE DES ETUDIANTS + H += [ + '
', + _make_listes_sem(formsemestre), + "
", + ] + # --- Lien mail enseignants: + adrlist = list(mails_enseignants - {None, ""}) + if adrlist: + H.append( + '

Courrier aux %d enseignants du semestre

' + % (",".join(adrlist), len(adrlist)) + ) + return "".join(H) + html_sco_header.sco_footer() + + +_TABLEAU_MODULES_HEAD = """ + + + + + + + + + +""" +_TABLEAU_MODULES_FOOT = """
CodeModuleInscritsResponsableCoefs.Évaluations
""" + + +def formsemestre_tableau_modules( + modimpls: list[dict], + nt, + formsemestre_id: int, + can_edit=True, + show_ues=True, + use_ue_coefs=False, +) -> str: + "Lignes table HTML avec modules du semestre" + H = [] + prev_ue_id = None + for modimpl in modimpls: + mod: Module = Module.query.get(modimpl["module_id"]) + mod_descr = "Module " + (mod.titre or "") + if mod.is_apc(): + coef_descr = ", ".join( + [f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()] + ) + if coef_descr: + mod_descr += " Coefs: " + coef_descr + else: + mod_descr += " (pas de coefficients) " + else: + mod_descr += ", coef. " + str(mod.coefficient) + mod_ens = sco_users.user_info(modimpl["responsable_id"])["nomcomplet"] + if modimpl["ens"]: + mod_ens += " (resp.), " + ", ".join( + [sco_users.user_info(e["ens_id"])["nomcomplet"] for e in modimpl["ens"]] + ) + mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( + moduleimpl_id=modimpl["moduleimpl_id"] + ) + + ue = modimpl["ue"] + if show_ues and (prev_ue_id != ue["ue_id"]): + prev_ue_id = ue["ue_id"] + titre = ue["titre"] + if use_ue_coefs: + titre += " (coef. %s)" % (ue["coefficient"] or 0.0) + H.append( + f""" + {ue["acronyme"]} + {titre} + """ + ) + + expr = sco_compute_moy.get_ue_expression( + formsemestre_id, ue["ue_id"], html_quote=True + ) + if expr: + H.append( + f""" {expr} + formule inutilisée en 9.2: supprimer""" + ) + + H.append("") + + if modimpl["ue"]["type"] != sco_codes_parcours.UE_STANDARD: + fontorange = " fontorange" # style css additionnel + else: + fontorange = "" + + etat = sco_evaluations.do_evaluation_etat_in_mod(nt, modimpl["moduleimpl_id"]) + # if nt.parcours.APC_SAE: + # tbd style si module non conforme + if ( + etat["nb_evals_completes"] > 0 + and etat["nb_evals_en_cours"] == 0 + and etat["nb_evals_vides"] == 0 + ): + H.append(f'') + else: + H.append(f'') + + H.append( + f"""{mod.code}""" + ) + H.append( + f"""{mod.abbrev or mod.titre or ""} + + {len(mod_inscrits)} + + { sco_users.user_info(modimpl["responsable_id"])["prenomnom"] } + + + """ + ) + if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): + coefs = mod.ue_coefs_list() + H.append(f'') + for coef in coefs: + if coef[1] > 0: + H.append( + f"""""" + ) + else: + H.append("""""") + H.append("") + H.append("") + if mod.module_type in ( + None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs + ModuleType.STANDARD, + ModuleType.RESSOURCE, + ModuleType.SAE, + ): + H.append('') + nb_evals = ( + etat["nb_evals_completes"] + + etat["nb_evals_en_cours"] + + etat["nb_evals_vides"] + ) + if nb_evals != 0: + H.append( + '%s prévues, %s ok' + % (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"]) + ) + if etat["nb_evals_en_cours"] > 0: + H.append( + ', %s en cours' + % (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"]) + ) + if etat["attente"]: + H.append( + ' [en attente]' + % modimpl["moduleimpl_id"] + ) + elif mod.module_type == ModuleType.MALUS: + nb_malus_notes = sum( + [ + e["etat"]["nb_notes"] + for e in nt.get_mod_evaluation_etat_list(modimpl["moduleimpl_id"]) + ] + ) + H.append( + """ + malus (%d notes) + """ + % (modimpl["moduleimpl_id"], nb_malus_notes) + ) + else: + raise ValueError(f"Invalid module_type {mod.module_type}") # a bug + + H.append("") + return "\n".join(H) + + +# Expérimental +def get_formsemestre_etudids_sans_notes( + formsemestre: FormSemestre, res: ResultatsSemestre +) -> set[int]: + """Les étudis d'étudiants de ce semestre n'ayant aucune note + alors que d'autres en ont. + """ + # Il y a-t-il des notes prises en compte ? + # On regarde la moy. gen., qui pour les étudiants sans notes est NaN en classique + # ou nulle en APC. + if all(res.etud_moy_gen.eq(0.0, fill_value=0.0)): + return set() # tout est 0 ou NaN, empty set + etudids_sans_notes = set.intersection( + *[ + set.intersection(*m_res.evals_etudids_sans_note.values()) + for m_res in res.modimpls_results.values() + if m_res.evals_etudids_sans_note + ] + ) + nb_sans_notes = len(etudids_sans_notes) + if nb_sans_notes > 0 and nb_sans_notes < len( + formsemestre.get_inscrits(include_demdef=False) + ): + return etudids_sans_notes + return set() + + +def formsemestre_warning_etuds_sans_note( + formsemestre: FormSemestre, res: ResultatsSemestre +) -> str: + """Vérifie si on est dans la situation où certains (mais pas tous) étudiants + n'ont aucune note alors que d'autres en ont. + Ce cas se produit typiquement quand on inscrit un étudiant en cours de semestre. + Il est alors utile de proposer de mettre toutes ses notes à ABS, ATT ou EXC + pour éviter de laisser toutes les évaluations "incomplètes". + """ + etudids_sans_notes = get_formsemestre_etudids_sans_notes(formsemestre, res) + if not etudids_sans_notes: + return "" + nb_sans_notes = len(etudids_sans_notes) + if nb_sans_notes < 5: + # peu d'étudiants, affiche leurs noms + etuds: list[Identite] = sorted( + [Identite.query.get(etudid) for etudid in etudids_sans_notes], + key=lambda e: e.sort_key, + ) + noms = ", ".join( + [ + f"""{etud.nomprenom}""" + for etud in etuds + ] + ) + msg_etuds = ( + f"""{noms} n'{"a" if nb_sans_notes == 1 else "ont"} aucune note :""" + ) + else: + msg_etuds = f"""{nb_sans_notes} étudiants n'ont aucune note :""" + + return f""" + """ + + +def formsemestre_note_etuds_sans_notes(formsemestre_id: int, code: str = None): + """Vue affichant les étudiants sans notes""" + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() + res: ResultatsSemestre = res_sem.load_formsemestre_results(formsemestre) + etudids_sans_notes = get_formsemestre_etudids_sans_notes(formsemestre, res) + etuds: list[Identite] = sorted( + [Identite.query.get(etudid) for etudid in etudids_sans_notes], + key=lambda e: e.sort_key, + ) + if request.method == "POST": + if not code in ("ATT", "EXC", "ABS"): + raise ScoValueError("code invalide: doit être ATT, ABS ou EXC") + for etud in etuds: + formsemestre.etud_set_all_missing_notes(etud, code) + flash(f"Notes de {len(etuds)} étudiants affectées à {code}") + return redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + ) + noms = "
  • ".join( + [ + f"""{etud.nomprenom}""" + for etud in etuds + ] + ) + return f""" + {html_sco_header.sco_header( + page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}" + )} +
    + {formsemestre_status_head( + formsemestre_id=formsemestre_id, page_title="Étudiants sans notes" + )} +
    +

    Étudiants sans notes:

    +
      +
    • {noms}
    • +
    + +
    + + Mettre toutes les notes de ces étudiants à : + + +
    + {html_sco_header.sco_footer()} + """ diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index d110f3ba..5e114677 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -40,6 +40,7 @@ from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Evaluation, FormSemestre from app.models import ScolarNews +from app.models.etudiants import Identite import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType import app.scodoc.notesdb as ndb @@ -300,6 +301,25 @@ def do_evaluation_upload_xls(): return 0, msg + "

    (pas de notes modifiées)

    " +def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -> bool: + """Enregistre la note d'un seul étudiant + value: valeur externe (float ou str) + """ + if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id): + raise AccessDenied(f"Modification des notes impossible pour {current_user}") + # Convert and check value + L, invalids, _, _, _ = _check_notes( + [(etud.id, value)], evaluation.to_dict(), evaluation.moduleimpl.module.to_dict() + ) + if len(invalids) == 0: + nb_changed, _, _ = notes_add( + current_user, evaluation.id, L, "Initialisation notes" + ) + if nb_changed == 1: + return True + return False # error + + def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): """Initialisation des notes manquantes""" evaluation = Evaluation.query.get_or_404(evaluation_id) @@ -318,7 +338,7 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False): for etudid, _ in etudid_etats: # pour tous les inscrits if etudid not in notes_db: # pas de note notes.append((etudid, value)) - # Check value + # Convert and check values L, invalids, _, _, _ = _check_notes( notes, evaluation.to_dict(), modimpl.module.to_dict() ) diff --git a/app/views/notes.py b/app/views/notes.py index 5b4dd416..28d1852a 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -203,6 +203,12 @@ sco_publish( Permission.ScoImplement, methods=["GET", "POST"], ) +sco_publish( + "/formsemestre_view_etuds_sans_note", + sco_formsemestre_status.formsemestre_note_etuds_sans_notes, + Permission.ScoView, + methods=["GET", "POST"], +) sco_publish( "/formsemestre_recapcomplet", sco_recapcomplet.formsemestre_recapcomplet,