forked from ScoDoc/ScoDoc
522 lines
19 KiB
Python
522 lines
19 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2023 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 import ScoDocJSONEncoder
|
|
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=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.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 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", "") 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"]:
|
|
nbabs, nbabsjust = sco_abs.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
|
|
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"] = etat["mini"] # chaine, sur 20
|
|
eval_dict["max"] = etat["maxi"]
|
|
if prefs["bul_show_moypromo"]:
|
|
eval_dict["moy"] = 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,
|
|
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
|