diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 7e41bd320d..dd64ce9662 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -33,6 +33,7 @@ class ResultatsSemestreBUT: "etud_moy_ue", "modimpls_evals_poids", "modimpls_evals_notes", + "etud_moy_gen", ) def __init__(self, formsemestre): diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py new file mode 100644 index 0000000000..9e234dc0ba --- /dev/null +++ b/app/but/bulletin_but_xml_compat.py @@ -0,0 +1,326 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 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 XML / compatibilité ScoDoc 7 + + => exporte quelques résultats BUT dans le format des anciens bulletins XML ScoDoc 7 + afin d'avoir un affichage acceptable sur les ENT anciens. + +Les plate-formes modernes utilisent uniquement la version JSON (but/bulletin_but.py) +""" + + +import datetime +from xml.etree import ElementTree +from xml.etree.ElementTree import Element + +from app import log +from app.but import bulletin_but +from app.models import FormSemestre, Identite +import app.scodoc.sco_utils as scu +import app.scodoc.notesdb as ndb +from app.scodoc import sco_abs +from app.scodoc import sco_codes_parcours +from app.scodoc import sco_edit_ue +from app.scodoc import sco_etud +from app.scodoc import sco_photos +from app.scodoc import sco_preferences +from app.scodoc import sco_xml + + +def bulletin_but_xml_compat( + formsemestre_id, + etudid, + doc=None, # XML document + force_publishing=False, + xml_nodate=False, + xml_with_decisions=False, # inclue les decisions même si non publiées + version="long", +) -> str: + """Bulletin XML au format ScoDoc 7, avec informations "BUT" """ + from app.scodoc import sco_bulletins + + log( + "bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )" + % (formsemestre_id, etudid) + ) + sem = FormSemestre.query.get_or_404(formsemestre_id) + etud = Identite.query.get_or_404(etudid) + results = bulletin_but.ResultatsSemestreBUT(sem) + nb_inscrits = len(results.etuds) + if sem.bul_hide_xml or force_publishing: + published = "1" + else: + published = "0" + if xml_nodate: + docdate = "" + else: + docdate = datetime.datetime.now().isoformat() + el = { + "etudid": str(etudid), + "formsemestre_id": str(formsemestre_id), + "date": docdate, + "publie": published, + } + if sem.etapes: + el["etape_apo"] = sem.etapes[0].etape_apo or "" + n = 2 + for et in sem.etapes[1:]: + el["etape_apo" + str(n)] = et.etape_apo or "" + n += 1 + x = Element("bulletinetud", **el) + if doc: + is_appending = True + doc.append(x) + else: + is_appending = False + doc = x + # Infos sur l'etudiant + doc.append( + Element( + "etudiant", + etudid=str(etudid), + code_nip=etud.code_nip or "", + code_ine=etud.code_ine or "", + nom=scu.quote_xml_attr(etud.nom), + prenom=scu.quote_xml_attr(etud.prenom), + civilite=scu.quote_xml_attr(etud.civilite_str()), + sexe=scu.quote_xml_attr(etud.civilite_str()), # compat + photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)), + email=scu.quote_xml_attr(etud.get_first_email() or ""), + emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""), + ) + ) + # Disponible pour publication ? + if not published: + return doc # stop ! + # Moyenne générale: + doc.append( + Element( + "note", + value=scu.fmt_note(results.etud_moy_gen[etud.id]), + min=scu.fmt_note(results.etud_moy_gen.min()), + max=scu.fmt_note(results.etud_moy_gen.max()), + moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen. + ) + ) + rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative + bonus = 0 # XXX TODO valeur du bonus sport + doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits))) + # XXX TODO: ajouter "rang_group" : rangs dans les partitions + doc.append(Element("note_max", value="20")) # notes toujours sur 20 + doc.append(Element("bonus_sport_culture", value=str(bonus))) + # Liste les UE / modules /evals + for ue in results.ues: + rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE + nb_inscrits_ue = ( + nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE" + ) + x_ue = Element( + "ue", + id=str(ue.id), + numero=scu.quote_xml_attr(ue.numero), + acronyme=scu.quote_xml_attr(ue.acronyme or ""), + titre=scu.quote_xml_attr(ue.titre or ""), + code_apogee=scu.quote_xml_attr(ue.code_apogee or ""), + ) + doc.append(x_ue) + if ue.type != sco_codes_parcours.UE_SPORT: + v = results.etud_moy_ue[ue.id][etud.id] + else: + v = 0 # XXX TODO valeur bonus sport pour cet étudiant + x_ue.append( + Element( + "note", + value=scu.fmt_note(v), + min=scu.fmt_note(results.etud_moy_ue[ue.id].min()), + max=scu.fmt_note(results.etud_moy_ue[ue.id].max()), + ) + ) + x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0))) + x_ue.append(Element("rang", value=str(rang_ue))) + x_ue.append(Element("effectif", value=str(nb_inscrits_ue))) + # Liste les modules rattachés à cette UE + for modimpl in results.modimpls: + # Liste ici uniquement les modules rattachés à cette UE + if modimpl.module.ue.id == ue.id: + mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id]) + coef = results.modimpl_coefs_df[modimpl.id][ue.id] + x_mod = Element( + "module", + id=str(modimpl.id), + code=str(modimpl.module.code or ""), + coefficient=str(coef), + numero=str(modimpl.module.numero or 0), + titre=scu.quote_xml_attr(modimpl.module.titre or ""), + abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""), + code_apogee=scu.quote_xml_attr(modimpl.module.code_apogee or ""), + ) + x_ue.append(x_mod) + x_mod.append( + Element( + "note", + value=mod_moy, + min=scu.fmt_note(results.etud_moy_ue[ue.id].min()), + max=scu.fmt_note(results.etud_moy_ue[ue.id].max()), + moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()), + ) + ) + # XXX TODO rangs et effectifs + # --- notes de chaque eval: + if version != "short": + for e in modimpl.evaluations: + if e.visibulletin or version == "long": + x_eval = Element( + "evaluation", + jour=e.jour.isoformat() if e.jour else "", + heure_debut=e.heure_debut.isoformat() + if e.heure_debut + else "", + heure_fin=e.heure_fin.isoformat() + if e.heure_debut + else "", + coefficient=str(e.coefficient), + # pas les poids en XML compat + evaluation_type=str(e.evaluation_type), + description=scu.quote_xml_attr(e.description), + # notes envoyées sur 20, ceci juste pour garder trace: + note_max_origin=str(e.note_max), + ) + x_mod.append(x_eval) + x_eval.append( + Element( + "note", + value=scu.fmt_note( + results.modimpls_evals_notes[e.moduleimpl_id][ + e.id + ][etud.id] + ), + ) + ) + # XXX TODO: Evaluations incomplètes ou futures: XXX + # XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante) + + # --- Absences + if sco_preferences.get_preference("bul_show_abs", formsemestre_id): + nbabs, nbabsjust = sem.get_abs_count(etud.id) + doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust))) + + # -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py --------- + # TODO : refactoring + + # --- Decision Jury + if ( + sco_preferences.get_preference("bul_show_decision", formsemestre_id) + or xml_with_decisions + ): + infos, dpv = sco_bulletins.etud_descr_situation_semestre( + etudid, + formsemestre_id, + format="xml", + show_uevalid=sco_preferences.get_preference( + "bul_show_uevalid", formsemestre_id + ), + ) + x_situation = Element("situation") + x_situation.text = scu.quote_xml_attr(infos["situation"]) + doc.append(x_situation) + if dpv: + decision = dpv["decisions"][0] + etat = decision["etat"] + if decision["decision_sem"]: + code = decision["decision_sem"]["code"] or "" + else: + code = "" + if ( + decision["decision_sem"] + and "compense_formsemestre_id" in decision["decision_sem"] + ): + doc.append( + Element( + "decision", + code=code, + etat=str(etat), + compense_formsemestre_id=str( + decision["decision_sem"]["compense_formsemestre_id"] or "" + ), + ) + ) + else: + doc.append(Element("decision", code=code, etat=str(etat))) + + 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] + doc.append( + Element( + "decision_ue", + ue_id=str(ue["ue_id"]), + numero=scu.quote_xml_attr(ue["numero"]), + acronyme=scu.quote_xml_attr(ue["acronyme"]), + titre=scu.quote_xml_attr(ue["titre"]), + code=decision["decisions_ue"][ue_id]["code"], + ) + ) + + for aut in decision["autorisations"]: + doc.append( + Element( + "autorisation_inscription", semestre_id=str(aut["semestre_id"]) + ) + ) + else: + doc.append(Element("decision", code="", etat="DEM")) + # --- Appreciations + cnx = ndb.GetDBConnexion() + apprecs = sco_etud.appreciations_list( + cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} + ) + for appr in apprecs: + x_appr = Element( + "appreciation", + date=ndb.DateDMYtoISO(appr["date"]), + ) + x_appr.text = scu.quote_xml_attr(appr["comment"]) + doc.append(x_appr) + + if is_appending: + return None + else: + return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) + + +""" +formsemestre_id=718 +etudid=12496 +from app.but.bulletin_but import * +mapp.set_sco_dept("RT") +sem = FormSemestre.query.get(formsemestre_id) +r = ResultatsSemestreBUT(sem) +""" diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 892e862d5b..13a71c941e 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -66,6 +66,10 @@ class Identite(db.Model): else: return self.nom + def get_first_email(self, field="email") -> str: + "le mail associé à la première adrese de l'étudiant, ou None" + return self.adresses[0].email or None if self.adresses.count() > 0 else None + def to_dict_bul(self): """Infos exportées dans les bulletins""" return { @@ -75,12 +79,8 @@ class Identite(db.Model): "date_naissance": self.date_naissance.isoformat() if self.date_naissance else None, - "email": self.adresses[0].email or None - if self.adresses.count() > 0 - else None, - "emailperso": self.adresses[0].emailperso or None - if self.adresses.count() > 0 - else None, + "email": self.get_first_email(), + "emailperso": self.get_first_email("emailperso"), "etudid": self.id, "nom": self.nom_disp(), "photo_url": sco_photos.get_etud_photo_url(self.id), diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index f564a41bd1..7126199aec 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -229,6 +229,17 @@ class FormSemestre(db.Model): return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" + def get_abs_count(self, etudid): + """Les comptes d'absences de cet étudiant dans ce semestre: + tuple (nb abs non justifiées, nb abs justifiées) + Utilise un cache. + """ + from app.scodoc import sco_abs + + return sco_abs.get_abs_count_in_interval( + etudid, self.date_debut.isoformat(), self.date_fin.isoformat() + ) + # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre notes_formsemestre_responsables = db.Table( diff --git a/app/models/ues.py b/app/models/ues.py index e3cd05a026..5f99bdc281 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -82,3 +82,18 @@ class UniteEns(db.Model): self.semestre_idx = module.semestre_id db.session.add(self) db.session.commit() + + def get_ressources(self): + "Liste des modules ressources rattachés à cette UE" + return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all() + + def get_saes(self): + "Liste des modules SAE rattachés à cette UE" + return self.modules.filter_by(module_type=scu.ModuleType.SAE).all() + + def get_modules_not_apc(self): + "Listes des modules non SAE et non ressource (standards, mais aussi bonus...)" + return self.modules.filter( + (Module.module_type != scu.ModuleType.SAE), + (Module.module_type != scu.ModuleType.RESSOURCE), + ).all() diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index a0aff43054..4234dcfe8b 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -47,6 +47,8 @@ from xml.etree.ElementTree import Element import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log +from app.but.bulletin_but_xml_compat import bulletin_but_xml_compat +from app.models.formsemestre import FormSemestre from app.scodoc import sco_abs from app.scodoc import sco_codes_parcours from app.scodoc import sco_cache @@ -78,6 +80,18 @@ def make_xml_formsemestre_bulletinetud( log("xml_bulletin( formsemestre_id=%s, etudid=%s )" % (formsemestre_id, etudid)) sem = sco_formsemestre.get_formsemestre(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if formsemestre.formation.is_apc: + return bulletin_but_xml_compat( + formsemestre_id, + etudid, + doc=doc, + force_publishing=force_publishing, + xml_nodate=xml_nodate, + xml_with_decisions=xml_with_decisions, # inclue les decisions même si non publiées + version=version, + ) + if (not sem["bul_hide_xml"]) or force_publishing: published = "1" else: