# -*- 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 # ############################################################################## """Génération du bulletin en format JSON (formations classiques) """ import datetime import json from flask import abort from app import db, ScoDocJSONEncoder from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.formations import edit_ue from app.models import but_validations from app.models import BulAppreciations, Evaluation, Matiere, UniteEns from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import sco_assiduites from app.scodoc import sco_evaluations from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc import sco_etud from app.scodoc.sco_preferences import SemPreferences from app.scodoc.sco_xml import quote_xml_attr # -------- Bulletin en JSON def make_json_formsemestre_bulletinetud( formsemestre_id: int, etudid: int, xml_with_decisions=False, version="long", force_publishing=False, # force publication meme si semestre non publie sur "portail" ) -> str: """Renvoie bulletin en chaine JSON""" d = formsemestre_bulletinetud_published_dict( formsemestre_id, etudid, force_publishing=force_publishing, xml_with_decisions=xml_with_decisions, version=version, ) return json.dumps(d, cls=ScoDocJSONEncoder) # (fonction séparée: n'utilise pas formsemestre_bulletinetud_dict() # pour simplifier le code, mais attention a la maintenance !) # def formsemestre_bulletinetud_published_dict( formsemestre_id, etudid, force_publishing=False, xml_nodate=False, xml_with_decisions=False, # inclure les décisions même si non publiées version="long", ) -> dict: """Dictionnaire representant les informations _publiees_ du bulletin de notes Utilisé pour JSON des formations classiques (mais pas pour le XML, qui est deprecated). version: short (sans les évaluations) long (avec les évaluations) # non implémenté: short_mat (sans évaluations, et structuration en matières) # long_mat (avec évaluations, et structuration en matières) """ from app.scodoc import sco_bulletins with_matieres = False if version.endswith("_mat"): version = version[:-4] # enlève le "_mat" with_matieres = True formsemestre = FormSemestre.get_formsemestre(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id) etud = Identite.get_etud(etudid) sem = sco_formsemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if etudid not in nt.identdict: abort(404, "etudiant non inscrit dans ce semestre") d = {"type": "classic", "version": "0"} published = (not formsemestre.bul_hide_xml) or force_publishing if xml_nodate: docdate = "" else: docdate = datetime.datetime.now().isoformat() el = { "etudid": etudid, "formsemestre_id": formsemestre_id, "date": docdate, "publie": published, "etapes": sem["etapes"], } # backward compat: if sem["etapes"]: el["etape_apo"] = sem["etapes"][0] or "" n = 2 for et in sem["etapes"][1:]: el["etape_apo" + str(n)] = et or "" n += 1 d.update(**el) # Infos sur l'etudiant etudinfo = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] d["etudiant"] = dict( etudid=etudid, code_nip=etudinfo["code_nip"], code_ine=etudinfo["code_ine"], nom=quote_xml_attr(etudinfo["nom"]), prenom=quote_xml_attr(etudinfo["prenom"]), civilite=quote_xml_attr(etudinfo["civilite_str"]), photo_url=quote_xml_attr(sco_photos.etud_photo_url(etudinfo, fast=True)), email=quote_xml_attr(etudinfo["email"]), emailperso=quote_xml_attr(etudinfo["emailperso"]), ) d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients # Disponible pour publication ? d["publie"] = published if not published: return d # stop ! etat_inscription = etud.inscription_etat(formsemestre.id) if etat_inscription != scu.INSCRIT: d.update(dict_decision_jury(etud, formsemestre, with_decisions=True)) return d # Groupes: partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False) partitions_etud_groups = {} # { partition_id : { etudid : group } } for partition in partitions: pid = partition["partition_id"] partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid) # Il serait préférable de factoriser et d'avoir la même section # "semestre" que celle des bulletins BUT. etud_groups = sco_groups.get_etud_formsemestre_groups( etud, formsemestre, only_to_show=True ) d["semestre"] = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "date_debut": formsemestre.date_debut.isoformat(), "date_fin": formsemestre.date_fin.isoformat(), "annee_universitaire": formsemestre.annee_scolaire_str(), "numero": formsemestre.semestre_id, "inscription": "", # inutilisé mais nécessaire pour le js de Seb. "groupes": [group.to_dict() for group in etud_groups], } ues_stat = nt.get_ues_stat_dict() modimpls = nt.get_modimpls_dict() nbetuds = len(nt.etud_moy_gen_ranks) moy_gen = scu.fmt_note(nt.get_etud_moy_gen(etudid)) if nt.get_moduleimpls_attente() or not prefs["bul_show_rangs"]: # n'affiche pas le rang sur le bulletin s'il y a des # notes en attente dans ce semestre rang = "" rang_gr = {} ninscrits_gr = {} else: rang = str(nt.get_etud_rang(etudid)) rang_gr, ninscrits_gr, gr_name = sco_bulletins.get_etud_rangs_groups( etudid, partitions, partitions_etud_groups, nt ) d["note"] = dict( value=moy_gen, min=scu.fmt_note(nt.moy_min), max=scu.fmt_note(nt.moy_max), moy=scu.fmt_note(nt.moy_moy), ) d["rang"] = dict(value=rang, ninscrits=nbetuds) d["rang_group"] = [] if rang_gr: for partition in partitions: d["rang_group"].append( dict( group_type=partition["partition_name"], group_name=gr_name[partition["partition_id"]], value=rang_gr[partition["partition_id"]], ninscrits=ninscrits_gr[partition["partition_id"]], ) ) d["note_max"] = dict(value=20) # notes toujours sur 20 d["bonus_sport_culture"] = dict( value=nt.bonus[etudid] if nt.bonus is not None else 0.0 ) # Liste les UE / modules /evals d["ue"] = [] d["ue_capitalisee"] = [] for ue_st in ues_stat: ue_id = ue_st["ue_id"] ue_status = nt.get_etud_ue_status(etudid, ue_id) if ue_st["ects"] is None: ects_txt = "" else: ects_txt = f"{ue_st['ects']:2.3g}" rang, effectif = nt.get_etud_ue_rang(ue_id, etudid) u = dict( id=ue_id, numero=quote_xml_attr(ue_st["numero"]), acronyme=quote_xml_attr(ue_st["acronyme"]), titre=quote_xml_attr(ue_st["titre"]), note=dict( value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""), min=scu.fmt_note(ue_st["min"]), max=scu.fmt_note(ue_st["max"]), moy=scu.fmt_note(ue_st["moy"]), ), rang=rang, effectif=effectif, ects=ects_txt, code_apogee=quote_xml_attr(ue_st["code_apogee"]), ) d["ue"].append(u) if with_matieres: u["module"] = [] # Structure UE/Matière/Module # Recodé en 2022 ue = db.session.get(UniteEns, ue_id) u["matiere"] = [ { "matiere_id": mat.id, "note": scu.fmt_note(nt.get_etud_mat_moy(mat.id, etudid)), "titre": mat.titre, "module": _list_modimpls( nt, etudid, [ mod for mod in modimpls if mod["module"]["matiere_id"] == mat.id ], prefs, version, ), } for mat in ue.matieres.order_by(Matiere.numero) ] else: # Liste les modules de l'UE u["module"] = _list_modimpls( nt, etudid, [mod for mod in modimpls if mod["module"]["ue_id"] == ue_id], prefs, version, ) # UE capitalisée (listée seulement si meilleure que l'UE courante) if ue_status["is_capitalized"]: try: ects_txt = str(int(ue_status["ue"].get("ects", "") or 0.0)) except ValueError: ects_txt = "" d["ue_capitalisee"].append( dict( id=ue_id, numero=quote_xml_attr(ue_st["numero"]), acronyme=quote_xml_attr(ue_st["acronyme"]), titre=quote_xml_attr(ue_st["titre"]), note=scu.fmt_note(ue_status["moy"]), coefficient_ue=scu.fmt_note(ue_status["coef_ue"]), date_capitalisation=ndb.DateDMYtoISO(ue_status["event_date"]), ects=ects_txt, ) ) # --- Absences if prefs["bul_show_abs"]: _, nbabsjust, nbabs = sco_assiduites.get_assiduites_count(etudid, sem) d["absences"] = dict(nbabs=nbabs, nbabsjust=nbabsjust) # --- Décision Jury d.update(dict_decision_jury(etud, formsemestre, with_decisions=xml_with_decisions)) # --- Appréciations appreciations = BulAppreciations.get_appreciations_list(formsemestre.id, etudid) d["appreciation"] = [ { "comment": quote_xml_attr(appreciation.comment), "date": appreciation.date.isoformat() if appreciation.date else "", } for appreciation in appreciations ] # return d def _list_modimpls( nt: NotesTableCompat, etudid: int, modimpls: list[dict], prefs: SemPreferences, version: str, ) -> list[dict]: modules_dict = [] for modimpl in modimpls: mod_moy = scu.fmt_note(nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)) if mod_moy == "NI": # ne mentionne pas les modules ou n'est pas inscrit continue modimpl_results = nt.modimpls_results.get(modimpl["moduleimpl_id"]) mod = modimpl["module"] # if mod['ects'] is None: # ects = '' # else: # ects = str(mod['ects']) modstat = nt.get_mod_stats(modimpl["moduleimpl_id"]) mod_dict = dict( id=modimpl["moduleimpl_id"], code=mod["code"], coefficient=mod["coefficient"], numero=mod["numero"], titre=quote_xml_attr(mod["titre"]), abbrev=quote_xml_attr(mod["abbrev"]), # ects=ects, ects des modules maintenant inutilisés note=dict(value=mod_moy), code_apogee=quote_xml_attr(mod["code_apogee"]), matiere_id=mod["matiere_id"], ) mod_dict["note"].update(modstat) for k in ("min", "max", "moy"): # formatte toutes les notes mod_dict["note"][k] = scu.fmt_note(mod_dict["note"][k]) if prefs["bul_show_mod_rangs"] and nt.mod_rangs is not None: mod_dict["rang"] = dict( value=nt.mod_rangs[modimpl["moduleimpl_id"]][0][etudid] ) mod_dict["effectif"] = dict(value=nt.mod_rangs[modimpl["moduleimpl_id"]][1]) # --- notes de chaque eval: evaluations_completes = nt.get_modimpl_evaluations_completes( modimpl["moduleimpl_id"] ) mod_dict["evaluation"] = [] if version != "short": for e in evaluations_completes: if e.visibulletin or version == "long": # Note à l'évaluation: val = modimpl_results.evals_notes[e.id].get(etudid, "NP") # nb: val est NA si etud démissionnaire e_dict = e.to_dict_bul() e_dict["note"] = scu.fmt_note(val, note_max=e.note_max) if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]: # XXX à revoir pour utiliser modimplresult etat = sco_evaluations.do_evaluation_etat(e.id) if prefs["bul_show_minmax_eval"]: e_dict["min"] = etat["mini"] # chaine, sur 20 e_dict["max"] = etat["maxi"] if prefs["bul_show_moypromo"]: e_dict["moy"] = etat["moy"] mod_dict["evaluation"].append(e_dict) # Evaluations incomplètes ou futures: complete_eval_ids = {e.id for e in evaluations_completes} if prefs["bul_show_all_evals"]: evaluations: list[Evaluation] = Evaluation.query.filter_by( moduleimpl_id=modimpl["moduleimpl_id"] ).order_by(Evaluation.date_debut) # plus ancienne d'abord for e in evaluations: if e.id not in complete_eval_ids: e_dict = e.to_dict_bul() e_dict["incomplete"] = 1 mod_dict["evaluation"].append(e_dict) modules_dict.append(mod_dict) return modules_dict def dict_decision_jury( etud: Identite, formsemestre: FormSemestre, with_decisions: bool = False ) -> dict: """dict avec decision pour bulletins json - autorisation_inscription - decision : décision semestre - decision_annee : annee BUT - decision_ue : list des décisions UE - situation with_decision donne les décision même si bul_show_decision est faux. Si formation APC, indique aussi validations année et RCUEs Exemple: { 'autorisation_inscription': [{'semestre_id': 4}], 'decision': {'code': 'ADM', 'compense_formsemestre_id': None, 'date': '2022-01-21', 'etat': 'I'}, 'decision_ue': [ { 'acronyme': 'UE31', 'code': 'ADM', 'ects': 16.0, 'numero': 23, 'titre': 'Approfondissement métiers', 'ue_id': 1787 }, ... ], 'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: ' 'UE31, UE32. Diplôme obtenu.', 'diplomation' : 'Diplôme obtenu.' # (ou vide) } """ from app.scodoc import sco_bulletins prefs = sco_preferences.SemPreferences(formsemestre.id) d = {} if prefs["bul_show_decision"] or with_decisions: infos, dpv = sco_bulletins.etud_descr_situation_semestre( etud.id, formsemestre, show_uevalid=prefs["bul_show_uevalid"], ) d["diplomation"] = infos["diplomation"] d["situation"] = infos["situation"] d["diplome_dut120"] = infos["diplome_dut120"] d["diplome_dut120_descr"] = infos["diplome_dut120_descr"] if dpv: decision = dpv["decisions"][0] etat = decision["etat"] if decision["decision_sem"]: code = decision["decision_sem"]["code"] date = ndb.DateDMYtoISO( dpv["decisions"][0]["decision_sem"]["event_date"] ) else: code = "" date = "" d["decision"] = dict( code=code, etat=etat, date=date, ) if ( decision["decision_sem"] and "compense_formsemestre_id" in decision["decision_sem"] ): d["decision"]["compense_formsemestre_id"] = decision["decision_sem"][ "compense_formsemestre_id" ] d["decision_ue"] = [] if decision[ "decisions_ue" ]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee) for ue_id in decision["decisions_ue"].keys(): ue = edit_ue.ue_list({"ue_id": ue_id})[0] d["decision_ue"].append( dict( ue_id=ue["ue_id"], numero=ue["numero"], acronyme=ue["acronyme"], titre=ue["titre"], code=decision["decisions_ue"][ue_id]["code"], ects=ue["ects"] or "", ) ) d["autorisation_inscription"] = [] for aut in decision["autorisations"]: d["autorisation_inscription"].append( dict( semestre_id=aut["semestre_id"], date=aut["date"].isoformat() if aut["date"] else None, ) ) else: d["decision"] = dict(code="", etat="DEM") # Ajout jury BUT: if formsemestre.formation.is_apc(): d.update(but_validations.dict_decision_jury(etud, formsemestre)) return d