# -*- 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 # ############################################################################## """Génération du bulletin en format JSON """ import datetime import json from flask import abort from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import but_validations from app.models import Matiere, ModuleImpl, 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_abs 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_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=scu.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, # inclue les decisions même si non publiées version="long", ) -> dict: """Dictionnaire representant les informations _publiees_ du bulletin de notes Utilisé pour JSON, devrait l'être aussi pour XML. (todo) version: short (sans les évaluations) long (avec les évaluations) 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.query.get_or_404(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id) etud = Identite.query.get(etudid) sem = sco_formsemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if not etudid in nt.identdict: abort(404, "etudiant non inscrit dans ce semestre") d = {"type": "classic", "version": "0"} if (not sem["bul_hide_xml"]) or force_publishing: published = True else: published = False 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) 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 = UniteEns.query.get(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", ""))) except ValueError: ects_txt = "" d["ue_capitalisee"].append( dict( id=ue_id, numero=quote_xml_attr(ue.numero), acronyme=quote_xml_attr(ue.acronyme), titre=quote_xml_attr(ue.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"]: nbabs, nbabsjust = sco_abs.get_abs_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 cnx = ndb.GetDBConnexion() apprecs = sco_etud.appreciations_list( cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} ) d["appreciation"] = [] for app in apprecs: d["appreciation"].append( dict( comment=quote_xml_attr(app["comment"]), date=ndb.DateDMYtoISO(app["date"]), ) ) # return d def _list_modimpls( nt: NotesTableCompat, etudid: int, modimpls: list[ModuleImpl], 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 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: evals = nt.get_evals_in_mod(modimpl["moduleimpl_id"]) mod_dict["evaluation"] = [] if version != "short": for e in evals: if e["visibulletin"] or version == "long": val = e["notes"].get(etudid, {"value": "NP"})["value"] # nb: val est NA si etud démissionnaire val = scu.fmt_note(val, note_max=e["note_max"]) eval_dict = dict( jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True), heure_debut=ndb.TimetoISO8601( e["heure_debut"], null_is_empty=True ), heure_fin=ndb.TimetoISO8601(e["heure_fin"], null_is_empty=True), coefficient=e["coefficient"], evaluation_type=e["evaluation_type"], # CM : ajout pour permettre de faire le lien sur # les bulletins en ligne avec l'évaluation: evaluation_id=e["evaluation_id"], description=quote_xml_attr(e["description"]), note=val, ) if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]: etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) if prefs["bul_show_minmax_eval"]: eval_dict["min"] = scu.fmt_note(etat["mini"]) eval_dict["max"] = scu.fmt_note(etat["maxi"]) if prefs["bul_show_moypromo"]: eval_dict["moy"] = scu.fmt_note(etat["moy"]) mod_dict["evaluation"].append(eval_dict) # Evaluations incomplètes ou futures: complete_eval_ids = set([e["evaluation_id"] for e in evals]) if prefs["bul_show_all_evals"]: all_evals = sco_evaluation_db.do_evaluation_list( args={"moduleimpl_id": modimpl["moduleimpl_id"]} ) all_evals.reverse() # plus ancienne d'abord for e in all_evals: if e["evaluation_id"] not in complete_eval_ids: mod_dict["evaluation"].append( dict( jour=ndb.DateDMYtoISO(e["jour"], null_is_empty=True), heure_debut=ndb.TimetoISO8601( e["heure_debut"], null_is_empty=True ), heure_fin=ndb.TimetoISO8601( e["heure_fin"], null_is_empty=True ), coefficient=e["coefficient"], description=quote_xml_attr(e["description"]), incomplete="1", ) ) 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 - decision : décision semestre - 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.'} """ 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.id, show_uevalid=prefs["bul_show_uevalid"], ) d["situation"] = infos["situation"] 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 = sco_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"]) ) 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