# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 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 # ############################################################################## """Ancienne fonction de synthèse des information jury (pour formations classiques) """ from operator import itemgetter from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import ( Formation, FormSemestre, Identite, ScolarAutorisationInscription, UniteEns, but_validations, ) from app.scodoc import codes_cursus from app.scodoc import sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_cursus from app.scodoc import sco_cursus_dut import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu def dict_pvjury( formsemestre_id, etudids=None, with_prev=False, with_parcours_decisions=False, ): """Données pour édition jury etudids == None => tous les inscrits, sinon donne la liste des ids Si with_prev: ajoute infos sur code jury semestre precedent Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours Résultat: { 'date' : str = date de la decision la plus recente, format dd/mm/yyyy, 'formsemestre' : dict = formsemestre, 'is_apc' : bool, 'formation' : { 'acronyme' :, 'titre': ... } 'decisions' : [ { 'identite' : {'nom' :, 'prenom':, ...,}, 'etat' : I ou D ou DEF 'decision_sem' : {'code':, 'code_prev': }, 'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'ects' : float, 'event_date' :str = "dd/mm/yyyy", }, }, 'autorisations' : [ { 'semestre_id' : { ... } } ], 'validation_parcours' : True si parcours validé (diplome obtenu) 'parcours' : 'S1, S2, S3, S4, A1', 'prev_code' : code (calculé slt si with_prev), 'mention' : mention (en fct moy gen), 'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées) 'sum_ects_capitalises' : somme des ECTS des UE capitalisees }, ... ], 'decisions_dict' : { etudid : decision (comme ci-dessus) }, } """ formsemestre: FormSemestre = FormSemestre.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if etudids is None: etudids = nt.get_etudids() if not etudids: return {} cnx = ndb.GetDBConnexion() sem = sco_formsemestre.get_formsemestre(formsemestre_id) max_date = "0000-01-01" has_prev = False # vrai si au moins un etudiant a un code prev semestre_non_terminal = False # True si au moins un etudiant a un devenir decisions = [] D = {} # même chose que decisions, mais { etudid : dec } for etudid in etudids: etud = Identite.get_etud(etudid) situation_etud = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) semestre_non_terminal = ( semestre_non_terminal or situation_etud.semestre_non_terminal ) d = {} d["identite"] = nt.identdict[etudid] d["etat"] = nt.get_etud_etat( etudid ) # I|D|DEF (inscription ou démission ou défaillant) d["decision_sem"] = nt.get_etud_decision_sem(etudid) d["decisions_ue"] = nt.get_etud_decisions_ue(etudid) if formsemestre.formation.is_apc(): d.update(but_validations.dict_decision_jury(etud, formsemestre)) # id du dernier semestre (chronologiquement) dans lequel il a été inscrit: d["last_formsemestre_id"] = situation_etud.get_semestres()[-1] ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid) d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values()) ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"]) d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code) if d["decision_sem"] and codes_cursus.code_semestre_validant( d["decision_sem"]["code"] ): d["mention"] = scu.get_mention(nt.get_etud_moy_gen(etudid)) else: d["mention"] = "" # Versions "en français": (avec les UE capitalisées d'ailleurs) dec_ue_list = _descr_decisions_ues( nt, etudid, d["decisions_ue"], d["decision_sem"] ) # avec les UE capitalisées, donc des éventuels doublons: d["decisions_ue_nb"] = len(dec_ue_list) # Mais sur la description (eg sur les bulletins), on ne veut pas # afficher ces doublons: on uniquifie sur ue_code _codes = set() ue_uniq = [] for ue in dec_ue_list: if ue["ue_code"] not in _codes: ue_uniq.append(ue) _codes.add(ue["ue_code"]) d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq]) if nt.is_apc: d["decision_sem_descr"] = "" # pas de validation de semestre en BUT else: d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"]) autorisations = ScolarAutorisationInscription.query.filter_by( etudid=etudid, origin_formsemestre_id=formsemestre_id ).all() d["autorisations"] = [a.to_dict() for a in autorisations] d["autorisations_descr"] = descr_autorisations(autorisations) d["validation_parcours"] = situation_etud.parcours_validated() d["parcours"] = situation_etud.get_cursus_descr(filter_futur=True) d["parcours_in_cur_formation"] = situation_etud.get_cursus_descr( filter_futur=True, filter_formation_code=True ) if with_parcours_decisions: d["parcours_decisions"] = situation_etud.get_parcours_decisions() # Observations sur les compensations: compensators = sco_cursus_dut.scolar_formsemestre_validation_list( cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid} ) obs = [] for compensator in compensators: # nb: il ne devrait y en avoir qu'un ! csem = sco_formsemestre.get_formsemestre(compensator["formsemestre_id"]) obs.append( "%s compensé par %s (%s)" % (sem["sem_id_txt"], csem["sem_id_txt"], csem["anneescolaire"]) ) if d["decision_sem"] and d["decision_sem"]["compense_formsemestre_id"]: compensed = sco_formsemestre.get_formsemestre( d["decision_sem"]["compense_formsemestre_id"] ) obs.append( f"""{sem["sem_id_txt"]} compense {compensed["sem_id_txt"]} ({compensed["anneescolaire"]})""" ) d["observation"] = ", ".join(obs) # Cherche la date de decision (sem ou UE) la plus récente: if d["decision_sem"]: date = ndb.DateDMYtoISO(d["decision_sem"]["event_date"]) if date and date > max_date: # decision plus recente max_date = date if d["decisions_ue"]: for dec_ue in d["decisions_ue"].values(): if dec_ue: date = ndb.DateDMYtoISO(dec_ue["event_date"]) if date and date > max_date: # decision plus recente max_date = date # Code semestre precedent if with_prev: # optionnel car un peu long... info = sco_etud.get_etud_info(etudid=etudid, filled=True) if not info: continue # should not occur etud = info[0] if situation_etud.prev_formsemestre and situation_etud.prev_decision: d["prev_decision_sem"] = situation_etud.prev_decision d["prev_code"] = situation_etud.prev_decision["code"] d["prev_code_descr"] = _descr_decision_sem( scu.INSCRIT, situation_etud.prev_decision ) d["prev"] = situation_etud.prev_formsemestre.to_dict() has_prev = True else: d["prev_decision_sem"] = None d["prev_code"] = "" d["prev_code_descr"] = "" d["Se"] = situation_etud decisions.append(d) D[etudid] = d return { "date": ndb.DateISOtoDMY(max_date), "formsemestre": sem, "is_apc": nt.is_apc, "has_prev": has_prev, "semestre_non_terminal": semestre_non_terminal, "formation": Formation.get_or_404(sem["formation_id"]).to_dict(), "decisions": decisions, "decisions_dict": D, } def _comp_ects_capitalises_by_ue_code(nt: NotesTableCompat, etudid: int): """Calcul somme des ECTS des UE capitalisees""" ues = nt.get_ues_stat_dict() ects_by_ue_code = {} for ue in ues: ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) if ue_status and ue_status["is_capitalized"]: ects_val = float(ue_status["ue"]["ects"] or 0.0) ects_by_ue_code[ue["ue_code"]] = ects_val return ects_by_ue_code def _comp_ects_by_ue_code(nt, decisions_ue): """Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées) decisions_ue est le resultat de nt.get_etud_decisions_ue Chaque resultat est un dict: { ue_code : ects } """ if not decisions_ue: return {} ects_by_ue_code = {} for ue_id in decisions_ue: d = decisions_ue[ue_id] ue = db.session.get(UniteEns, ue_id) ects_by_ue_code[ue.ue_code] = d["ects"] return ects_by_ue_code def descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str: "résumé textuel des autorisations d'inscription (-> 'S1, S3' )" return ", ".join([f"S{a.semestre_id}" for a in autorisations]) def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]: """Liste des UE validées dans ce semestre (incluant les UE capitalisées)""" if not decisions_ue: return [] uelist = [] # Les UE validées dans ce semestre: for ue_id in decisions_ue: if decisions_ue[ue_id] and ( codes_cursus.code_ue_validant(decisions_ue[ue_id].get("code")) or ( (not nt.is_apc) and ( # XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8 decision_sem and scu.CONFIG.CAPITALIZE_ALL_UES and decision_sem and codes_cursus.code_semestre_validant(decision_sem.get("code")) ) ) ): ue = db.session.get(UniteEns, ue_id) assert ue # note modernisation code: on utilise des dict tant que get_etud_ue_status renvoie des dicts uelist.append(ue.to_dict()) # Les UE capitalisées dans d'autres semestres: if etudid in nt.validations.ue_capitalisees.index: for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]: try: uelist.append(nt.get_etud_ue_status(etudid, ue_id)["ue"]) except (KeyError, TypeError): pass uelist.sort(key=itemgetter("numero")) return uelist def _descr_decision_sem(etat, decision_sem): "résumé textuel de la décision de semestre" if etat == "D": decision = "Démission" else: if decision_sem: cod = decision_sem["code"] decision = ( # exception pour AJ afin de ne pas mentionner le BUT codes_cursus.CODES_EXPL.get(cod, "") if cod != "AJ" else "Ajourné" ) else: decision = "" return decision def _sum_ects_dicts(s, t): """Somme deux dictionnaires { ue_code : ects }, quand une UE de même code apparait deux fois, prend celle avec le plus d'ECTS. """ sum_ects = sum(s.values()) + sum(t.values()) for ue_code in set(s).intersection(set(t)): sum_ects -= min(s[ue_code], t[ue_code]) return sum_ects