diff --git a/README.md b/README.md index 209a2a017..f571216ab 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes). ### État actuel (26 jan 22) - - 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: + - 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: - ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT. - - 9.2 (branche refactor_nt) est la version de développement. + - 9.2 (branche dev92) est la version de développement. ### Lignes de commandes diff --git a/app/__init__.py b/app/__init__.py index 74585a7b2..db5a15b08 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -295,10 +295,12 @@ def create_app(config_class=DevConfig): from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard + from app.but.bulletin_but_pdf import BulletinGeneratorStandardBUT from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC # l'ordre est important, le premier sera le "défaut" pour les nouveaux départements. sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard) + sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandardBUT) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC) if app.testing or app.debug: diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 5d4fd74f3..f64769fc9 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -9,14 +9,15 @@ import datetime from flask import url_for, g -from app.models.formsemestre import FormSemestre -from app.scodoc import sco_utils as scu -from app.scodoc import sco_bulletins_json -from app.scodoc import sco_preferences -from app.scodoc.sco_codes_parcours import UE_SPORT -from app.scodoc.sco_utils import fmt_note from app.comp.res_but import ResultatsSemestreBUT +from app.models import FormSemestre, Identite, formsemestre +from app.scodoc import sco_bulletins, sco_utils as scu +from app.scodoc import sco_bulletins_json +from app.scodoc import sco_bulletins_pdf +from app.scodoc import sco_preferences +from app.scodoc.sco_codes_parcours import UE_SPORT, DEF +from app.scodoc.sco_utils import fmt_note class BulletinBUT: @@ -28,6 +29,7 @@ class BulletinBUT: def __init__(self, formsemestre: FormSemestre): """ """ self.res = ResultatsSemestreBUT(formsemestre) + self.prefs = sco_preferences.SemPreferences(formsemestre.id) def etud_ue_mod_results(self, etud, ue, modimpls) -> dict: "dict synthèse résultats dans l'UE pour les modules indiqués" @@ -84,7 +86,7 @@ class BulletinBUT: "saes": self.etud_ue_mod_results(etud, ue, res.saes), } if ue.type != UE_SPORT: - if sco_preferences.get_preference("bul_show_ue_rangs", res.formsemestre.id): + if self.prefs["bul_show_ue_rangs"]: rangs, effectif = res.ue_rangs[ue.id] rang = rangs[etud.id] else: @@ -109,9 +111,10 @@ class BulletinBUT: d["modules"] = self.etud_mods_results(etud, modimpls_spo) return d - def etud_mods_results(self, etud, modimpls) -> dict: + def etud_mods_results(self, etud, modimpls, version="long") -> dict: """dict synthèse résultats des modules indiqués, - avec évaluations de chacun.""" + avec évaluations de chacun (sauf si version == "short") + """ res = self.res d = {} # etud_idx = self.etud_index[etud.id] @@ -152,14 +155,14 @@ class BulletinBUT: "evaluations": [ self.etud_eval_results(etud, e) for e in modimpl.evaluations - if e.visibulletin + if (e.visibulletin or version == "long") and ( modimpl_results.evaluations_etat[e.id].is_complete - or sco_preferences.get_preference( - "bul_show_all_evals", res.formsemestre.id - ) + or self.prefs["bul_show_all_evals"] ) - ], + ] + if version != "short" + else [], } return d @@ -216,13 +219,23 @@ class BulletinBUT: else: return f"Bonus de {fmt_note(bonus_vect.iloc[0])}" - def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict: - """Le bulletin de l'étudiant dans ce semestre. - Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai + def bulletin_etud( + self, + etud: Identite, + formsemestre: FormSemestre, + force_publishing=False, + version="long", + ) -> dict: + """Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML. + - version: + "long", "selectedevals": toutes les infos (notes des évaluations) + "short" : ne descend pas plus bas que les modules. + + - Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai (bulletins non publiés). """ res = self.res - etat_inscription = etud.etat_inscription(formsemestre.id) + etat_inscription = etud.inscription_etat(formsemestre.id) nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] published = (not formsemestre.bul_hide_xml) or force_publishing d = { @@ -239,7 +252,9 @@ class BulletinBUT: }, "formsemestre_id": formsemestre.id, "etat_inscription": etat_inscription, - "options": sco_preferences.bulletin_option_affichage(formsemestre.id), + "options": sco_preferences.bulletin_option_affichage( + formsemestre.id, self.prefs + ), } if not published: return d @@ -278,8 +293,10 @@ class BulletinBUT: ) d.update( { - "ressources": self.etud_mods_results(etud, res.ressources), - "saes": self.etud_mods_results(etud, res.saes), + "ressources": self.etud_mods_results( + etud, res.ressources, version=version + ), + "saes": self.etud_mods_results(etud, res.saes, version=version), "ues": { ue.acronyme: self.etud_ue_results(etud, ue) for ue in res.ues @@ -312,3 +329,54 @@ class BulletinBUT: ) return d + + def bulletin_etud_complet(self, etud: Identite) -> dict: + """Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf + Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict + """ + d = self.bulletin_etud(etud, self.res.formsemestre, force_publishing=True) + d["etudid"] = etud.id + d["etud"] = d["etudiant"] + d["etud"]["nomprenom"] = etud.nomprenom + d.update(self.res.sem) + etud_etat = self.res.get_etud_etat(etud.id) + d["filigranne"] = sco_bulletins_pdf.get_filigranne( + etud_etat, + self.prefs, + decision_sem=d["semestre"].get("decision_sem"), + ) + if etud_etat == scu.DEMISSION: + d["demission"] = "(Démission)" + elif etud_etat == DEF: + d["demission"] = "(Défaillant)" + else: + d["demission"] = "" + + # --- Absences + d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id) + + # --- Decision Jury + infos, dpv = sco_bulletins.etud_descr_situation_semestre( + etud.id, + self.res.formsemestre.id, + format="html", + show_date_inscr=self.prefs["bul_show_date_inscr"], + show_decisions=self.prefs["bul_show_decision"], + show_uevalid=self.prefs["bul_show_uevalid"], + show_mention=self.prefs["bul_show_mention"], + ) + + d.update(infos) + # --- Rangs + d[ + "rang_nt" + ] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}" + d["rang_txt"] = "Rang " + d["rang_nt"] + + # --- Appréciations + d.update( + sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id) + ) + d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"])) + + return d diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py new file mode 100644 index 000000000..5003e216e --- /dev/null +++ b/app/but/bulletin_but_pdf.py @@ -0,0 +1,116 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Génération bulletin BUT au format PDF standard +""" + +import datetime +from app.scodoc.sco_pdf import blue, cm, mm + +from flask import url_for, g +from app.models.formsemestre import FormSemestre + +from app.scodoc import gen_tables +from app.scodoc import sco_utils as scu +from app.scodoc import sco_bulletins_json +from app.scodoc import sco_preferences +from app.scodoc.sco_codes_parcours import UE_SPORT +from app.scodoc.sco_utils import fmt_note +from app.comp.res_but import ResultatsSemestreBUT + +from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard + + +class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): + """Génération du bulletin de BUT au format PDF. + + self.infos est le dict issu de BulletinBUT.bulletin_etud_complet() + """ + + list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur + + def bul_table(self, format="html"): + """Génère la table centrale du bulletin de notes + Renvoie: + - en HTML: une chaine + - en PDF: une liste d'objets PLATYPUS (eg instance de Table). + """ + formsemestre_id = self.infos["formsemestre_id"] + ( + synth_col_keys, + synth_P, + synth_pdf_style, + synth_col_widths, + ) = self.but_table_synthese() + # + table_synthese = gen_tables.GenTable( + rows=synth_P, + columns_ids=synth_col_keys, + pdf_table_style=synth_pdf_style, + pdf_col_widths=[synth_col_widths[k] for k in synth_col_keys], + preferences=self.preferences, + html_class="notes_bulletin", + html_class_ignore_default=True, + html_with_td_classes=True, + ) + # Ici on ajoutera table des ressources, tables des UE + # TODO + + # XXX à modifier pour générer plusieurs tables: + return table_synthese.gen(format=format) + + def but_table_synthese(self): + """La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes + et leurs coefs. + Renvoie: colkeys, P, pdf_style, colWidths + - colkeys: nom des colonnes de la table (clés) + - P : table (liste de dicts de chaines de caracteres) + - pdf_style : commandes table Platypus + - largeurs de colonnes pour PDF + """ + col_widths = { + "titre": None, + "moyenne": 2 * cm, + "coef": 2 * cm, + } + P = [] # elems pour générer table avec gen_table (liste de dicts) + col_keys = ["titre", "moyenne"] # noms des colonnes à afficher + for ue_acronym, ue in self.infos["ues"].items(): + # 1er ligne titre UE + moy_ue = ue.get("moyenne") + t = { + "titre": f"{ue_acronym} - {ue['titre']}", + "moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-", + "_css_row_class": "note_bold", + "_pdf_row_markup": ["b"], + "_pdf_style": [], + } + P.append(t) + # 2eme ligne titre UE (bonus/malus/ects) + t = { + "titre": "", + "moyenne": f"""Bonus: {ue['bonus']} - Malus: { + ue["malus"]} - ECTS: {ue["ECTS"]["acquis"]} / {ue["ECTS"]["total"]}""", + "_css_row_class": "note_bold", + "_pdf_row_markup": ["b"], + "_pdf_style": [ + ( + "LINEBELOW", + (0, 0), + (-1, 0), + self.PDF_LINEWIDTH, + self.PDF_LINECOLOR, + ) + ], + } + P.append(t) + + # Global pdf style commands: + pdf_style = [ + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu: + ] + return col_keys, P, pdf_style, col_widths diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 73e06c4de..bab7b7287 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -72,7 +72,7 @@ def bulletin_but_xml_compat( etud: Identite = Identite.query.get_or_404(etudid) results = bulletin_but.ResultatsSemestreBUT(formsemestre) nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT] - # etat_inscription = etud.etat_inscription(formsemestre.id) + # etat_inscription = etud.inscription_etat(formsemestre.id) etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat if (not formsemestre.bul_hide_xml) or force_publishing: published = 1 diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 18f13380d..060debc3b 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -4,12 +4,14 @@ et données rattachées (adresses, annotations, ...) """ +import datetime from functools import cached_property from flask import abort, url_for from flask import g, request import sqlalchemy +from sqlalchemy import desc, text -from app import db +from app import db, log from app import models from app.scodoc import notesdb as ndb @@ -82,6 +84,11 @@ class Identite(db.Model): return scu.suppress_accents(s) return s + @property + def e(self): + "terminaison en français: 'ne', '', 'ou '(e)'" + return {"M": "", "F": "e"}.get(self.civilite, "(e)") + def nom_disp(self) -> str: "Nom à afficher" if self.nom_usuel: @@ -123,7 +130,7 @@ class Identite(db.Model): 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 + return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None def to_dict_scodoc7(self): """Représentation dictionnaire, @@ -134,7 +141,7 @@ class Identite(db.Model): # ScoDoc7 output_formators: (backward compat) e["etudid"] = self.id e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"]) - e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)") + e["ne"] = self.e return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty def to_dict_bul(self, include_urls=True): @@ -153,6 +160,7 @@ class Identite(db.Model): "etudid": self.id, "nom": self.nom_disp(), "prenom": self.prenom, + "nomprenom": self.nomprenom, } if include_urls: d["fiche_url"] = url_for( @@ -172,6 +180,23 @@ class Identite(db.Model): ] return r[0] if r else None + def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]: + """Liste des inscriptions à des semestres _courants_ + (il est rare qu'il y en ai plus d'une, mais c'est possible). + Triées par date de début de semestre décroissante (le plus récent en premier). + """ + from app.models.formsemestre import FormSemestre, FormSemestreInscription + + return ( + FormSemestreInscription.query.join(FormSemestreInscription.formsemestre) + .filter( + FormSemestreInscription.etudid == self.id, + text("date_debut < now() and date_fin > now()"), + ) + .order_by(desc(FormSemestre.date_debut)) + .all() + ) + def inscription_courante_date(self, date_debut, date_fin): """La première inscription à un formsemestre incluant la période [date_debut, date_fin] @@ -183,8 +208,8 @@ class Identite(db.Model): ] return r[0] if r else None - def etat_inscription(self, formsemestre_id): - """etat de l'inscription de cet étudiant au semestre: + def inscription_etat(self, formsemestre_id): + """État de l'inscription de cet étudiant au semestre: False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF """ # voir si ce n'est pas trop lent: @@ -195,6 +220,110 @@ class Identite(db.Model): return ins.etat return False + def inscription_descr(self) -> dict: + """Description de l'état d'inscription""" + inscription_courante = self.inscription_courante() + if inscription_courante: + titre_sem = inscription_courante.formsemestre.titre_mois() + return { + "etat_in_cursem": inscription_courante.etat, + "inscription_courante": inscription_courante, + "inscription": titre_sem, + "inscription_str": "Inscrit en " + titre_sem, + "situation": self.descr_situation_etud(), + } + else: + if self.formsemestre_inscriptions: + # cherche l'inscription la plus récente: + fin_dernier_sem = max( + [ + inscr.formsemestre.date_debut + for inscr in self.formsemestre_inscriptions + ] + ) + if fin_dernier_sem > datetime.date.today(): + inscription = "futur" + situation = "futur élève" + else: + inscription = "ancien" + situation = "ancien élève" + else: + inscription = ("non inscrit",) + situation = inscription + return { + "etat_in_cursem": "?", + "inscription_courante": None, + "inscription": inscription, + "inscription_str": inscription, + "situation": situation, + } + + def descr_situation_etud(self) -> str: + """Chaîne décrivant la situation _actuelle_ de l'étudiant. + Exemple: + "inscrit en BUT R&T semestre 2 FI (Jan 2022 - Jul 2022) le 16/01/2022" + ou + "non inscrit" + """ + inscriptions_courantes = self.inscriptions_courantes() + if inscriptions_courantes: + inscr = inscriptions_courantes[0] + if inscr.etat == scu.INSCRIT: + situation = f"inscrit{self.e} en {inscr.formsemestre.titre_mois()}" + # Cherche la date d'inscription dans scolar_events: + events = models.ScolarEvent.query.filter_by( + etudid=self.id, + formsemestre_id=inscr.formsemestre.id, + event_type="INSCRIPTION", + ).all() + if not events: + log( + f"*** situation inconsistante pour {self} (inscrit mais pas d'event)" + ) + date_ins = "???" # ??? + else: + date_ins = events[0].event_date + situation += date_ins.strftime(" le %d/%m/%Y") + else: + situation = f"démission de {inscr.formsemestre.titre_mois()}" + # Cherche la date de demission dans scolar_events: + events = models.ScolarEvent.query.filter_by( + etudid=self.id, + formsemestre_id=inscr.formsemestre.id, + event_type="DEMISSION", + ).all() + if not events: + log( + f"*** situation inconsistante pour {self} (demission mais pas d'event)" + ) + date_dem = "???" # ??? + else: + date_dem = events[0].event_date + situation += date_dem.strftime(" le %d/%m/%Y") + else: + situation = "non inscrit" + self.e + + return situation + + def photo_html(self, title=None, size="small") -> str: + """HTML img tag for the photo, either in small size (h90) + or original size (size=="orig") + """ + from app.scodoc import sco_photos + + # sco_photo traite des dicts: + return sco_photos.etud_photo_html( + etud=dict( + etudid=self.id, + code_nip=self.code_nip, + nomprenom=self.nomprenom, + nom_disp=self.nom_disp(), + photo_filename=self.photo_filename, + ), + title=title, + size=size, + ) + def make_etud_args( etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 41ede854f..d92c375fb 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -12,7 +12,6 @@ from app import log from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN -from app.models import UniteEns import app.scodoc.sco_utils as scu from app.models.ues import UniteEns @@ -23,6 +22,7 @@ from app.scodoc import sco_codes_parcours from app.scodoc import sco_preferences from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_utils import MONTH_NAMES_ABBREV class FormSemestre(db.Model): @@ -122,6 +122,7 @@ class FormSemestre(db.Model): return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>" def to_dict(self): + "dict (compatible ScoDoc7)" d = dict(self.__dict__) d.pop("_sa_instance_state", None) # ScoDoc7 output_formators: (backward compat) @@ -162,8 +163,8 @@ class FormSemestre(db.Model): d["periode"] = 2 # typiquement, début en février: S2, S4... d["titre_num"] = self.titre_num() d["titreannee"] = self.titre_annee() - d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}" - d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}" + d["mois_debut"] = self.mois_debut() + d["mois_fin"] = self.mois_fin() d["titremois"] = "%s %s (%s - %s)" % ( d["titre_num"], self.modalite or "", @@ -293,6 +294,7 @@ class FormSemestre(db.Model): """chaîne "J. Dupond, X. Martin" ou "Jacques Dupond, Xavier Martin" """ + # was "nomcomplet" if not self.responsables: return "" if abbrev_prenom: @@ -304,6 +306,14 @@ class FormSemestre(db.Model): "2021 - 2022" return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) + def mois_debut(self) -> str: + "Oct 2021" + return f"{MONTH_NAMES_ABBREV[self.date_debut.month - 1]} {self.date_debut.year}" + + def mois_fin(self) -> str: + "Jul 2022" + return f"{MONTH_NAMES_ABBREV[self.date_fin.month - 1]} {self.date_debut.year}" + def session_id(self) -> str: """identifiant externe de semestre de formation Exemple: RT-DUT-FI-S1-ANNEE @@ -364,7 +374,7 @@ class FormSemestre(db.Model): 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) + tuple (nb abs, nb abs justifiées) Utilise un cache. """ from app.scodoc import sco_abs diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 0aa74ef4b..1935036e9 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -6,7 +6,8 @@ import flask_sqlalchemy from app import db from app.comp import df_cache -from app.models import Identite, Module +from app.models.etudiants import Identite +from app.models.modules import Module import app.scodoc.notesdb as ndb from app.scodoc import sco_utils as scu diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 855545556..72326132b 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -233,7 +233,10 @@ class GenTable(object): colspan_count -= 1 # if colspan_count > 0: # continue # skip cells after a span - content = row.get(cid, "") or "" # nota: None converted to '' + if pdf_mode: + content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or "" + else: + content = row.get(cid, "") or "" # nota: None converted to '' colspan = row.get("_%s_colspan" % cid, 0) if colspan > 1: pdf_style_list.append( @@ -547,9 +550,16 @@ class GenTable(object): omit_hidden_lines=True, ) try: - Pt = [ - [Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list - ] + Pt = [] + for line in data_list: + Pt.append( + [ + Paragraph(SU(str(x)), CellStyle) + if (not isinstance(x, Paragraph)) + else x + for x in line + ] + ) except ValueError as exc: raise ScoPDFFormatError(str(exc)) from exc pdf_style_list += self.pdf_table_style diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 733452967..071cbe8ef 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -1037,7 +1037,7 @@ def get_abs_count(etudid, sem): def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs, nb abs justifiées) Utilise un cache. """ key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index f15e7d4c8..466d13df1 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -35,6 +35,7 @@ import datetime from flask import g, url_for from flask_mail import Message +from app.models.formsemestre import FormSemestre import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -55,27 +56,30 @@ def abs_notify(etudid, date): """ from app.scodoc import sco_abs - sem = retreive_current_formsemestre(etudid, date) - if not sem: + formsemestre = retreive_current_formsemestre(etudid, date) + if not formsemestre: return # non inscrit a la date, pas de notification - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) - do_abs_notify(sem, etudid, date, nbabs, nbabsjust) + nbabs, nbabsjust = sco_abs.get_abs_count_in_interval( + etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat() + ) + do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust) -def do_abs_notify(sem, etudid, date, nbabs, nbabsjust): +def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust): """Given new counts of absences, check if notifications are requested and send them.""" # prefs fallback to global pref if sem is None: - if sem: - formsemestre_id = sem["formsemestre_id"] + if formsemestre: + formsemestre_id = formsemestre.id else: formsemestre_id = None - prefs = sco_preferences.SemPreferences(formsemestre_id=sem["formsemestre_id"]) + prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id) destinations = abs_notify_get_destinations( - sem, prefs, etudid, date, nbabs, nbabsjust + formsemestre, prefs, etudid, date, nbabs, nbabsjust ) - msg = abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust) + + msg = abs_notification_message(formsemestre, prefs, etudid, nbabs, nbabsjust) if not msg: return # abort @@ -131,19 +135,19 @@ def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id ) -def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust): +def abs_notify_get_destinations( + formsemestre: FormSemestre, prefs, etudid, date, nbabs, nbabsjust +) -> set: """Returns set of destination emails to be notified""" - formsemestre_id = sem["formsemestre_id"] destinations = [] # list of email address to notify - if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id): - if sem and prefs["abs_notify_respsem"]: + if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id): + if prefs["abs_notify_respsem"]: # notifie chaque responsable du semestre - for responsable_id in sem["responsables"]: - u = sco_users.user_info(responsable_id) - if u["email"]: - destinations.append(u["email"]) + for responsable in formsemestre.responsables: + if responsable.email: + destinations.append(responsable.email) if prefs["abs_notify_chief"] and prefs["email_chefdpt"]: destinations.append(prefs["email_chefdpt"]) if prefs["abs_notify_email"]: @@ -156,7 +160,7 @@ def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust): # Notification (à chaque fois) des resp. de modules ayant des évaluations # à cette date # nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas - if sem and prefs["abs_notify_respeval"]: + if prefs["abs_notify_respeval"]: mods = mod_with_evals_at_date(date, etudid) for mod in mods: u = sco_users.user_info(mod["responsable_id"]) @@ -232,7 +236,9 @@ def user_nbdays_since_last_notif(email_addr, etudid): return None -def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): +def abs_notification_message( + formsemestre: FormSemestre, prefs, etudid, nbabs, nbabsjust +): """Mime notification message based on template. returns a Message instance or None if sending should be canceled (empty template). @@ -242,7 +248,7 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] # Variables accessibles dans les balises du template: %(nom_variable)s : - values = sco_bulletins.make_context_dict(sem, etud) + values = sco_bulletins.make_context_dict(formsemestre, etud) values["nbabs"] = nbabs values["nbabsjust"] = nbabsjust @@ -264,9 +270,11 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): return msg -def retreive_current_formsemestre(etudid, cur_date): +def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre: """Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée date est une chaine au format ISO (yyyy-mm-dd) + + Result: FormSemestre ou None si pas inscrit à la date indiquée """ req = """SELECT i.formsemestre_id FROM notes_formsemestre_inscription i, notes_formsemestre sem @@ -278,8 +286,8 @@ def retreive_current_formsemestre(etudid, cur_date): if not r: return None # s'il y a plusieurs semestres, prend le premier (rarissime et non significatif): - sem = sco_formsemestre.get_formsemestre(r[0]["formsemestre_id"]) - return sem + formsemestre = FormSemestre.query.get(r[0]["formsemestre_id"]) + return formsemestre def mod_with_evals_at_date(date_abs, etudid): diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index bb0597e59..65dcb3a91 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -28,30 +28,19 @@ """Génération des bulletins de notes """ -from app.models import formsemestre -import time -import pprint import email -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.mime.base import MIMEBase -from email.header import Header -from reportlab.lib.colors import Color -import urllib +import time from flask import g, request -from flask import url_for +from flask import render_template, url_for from flask_login import current_user -from flask_mail import Message -from app.models.moduleimpls import ModuleImplInscription -import app.scodoc.sco_utils as scu -from app.scodoc.sco_utils import ModuleType -import app.scodoc.notesdb as ndb +from app import email from app import log +from app.but import bulletin_but from app.comp import res_sem from app.comp.res_common import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, Identite, ModuleImplInscription from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc import html_sco_header @@ -60,9 +49,9 @@ from app.scodoc import sco_abs from app.scodoc import sco_abs_views from app.scodoc import sco_bulletins_generator from app.scodoc import sco_bulletins_json +from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_xml from app.scodoc import sco_codes_parcours -from app.scodoc import sco_cache from app.scodoc import sco_etud from app.scodoc import sco_evaluation_db from app.scodoc import sco_formations @@ -73,7 +62,9 @@ from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc import sco_pvjury from app.scodoc import sco_users -from app import email +import app.scodoc.sco_utils as scu +from app.scodoc.sco_utils import ModuleType, fmt_note +import app.scodoc.notesdb as ndb # ----- CLASSES DE BULLETINS DE NOTES from app.scodoc import sco_bulletins_standard @@ -85,33 +76,20 @@ from app.scodoc import sco_bulletins_legacy from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun -def make_context_dict(sem, etud): +def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict: """Construit dictionnaire avec valeurs pour substitution des textes (preferences bul_pdf_*) """ - C = sem.copy() - C["responsable"] = " ,".join( - [ - sco_users.user_info(responsable_id)["prenomnom"] - for responsable_id in sem["responsables"] - ] - ) - - annee_debut = sem["date_debut"].split("/")[2] - annee_fin = sem["date_fin"].split("/")[2] - if annee_debut != annee_fin: - annee = "%s - %s" % (annee_debut, annee_fin) - else: - annee = annee_debut - C["anneesem"] = annee + C = formsemestre.get_infos_dict() + C["responsable"] = formsemestre.responsables_str() + C["anneesem"] = C["annee"] # backward compat C.update(etud) # copie preferences - # XXX devrait acceder directement à un dict de preferences, à revoir for name in sco_preferences.get_base_preferences().prefs_name: - C[name] = sco_preferences.get_preference(name, sem["formsemestre_id"]) + C[name] = sco_preferences.get_preference(name, formsemestre.id) # ajoute groupes et group_0, group_1, ... - sco_groups.etud_add_group_infos(etud, sem) + sco_groups.etud_add_group_infos(etud, formsemestre.id) C["groupes"] = etud["groupes"] n = 0 for partition_id in etud["partitions"]: @@ -132,7 +110,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): Le contenu du dictionnaire dépend des options (rangs, ...) et de la version choisie (short, long, selectedevals). - Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML. + Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...) + en HTML et PDF, mais pas ceux en XML. """ from app.scodoc import sco_abs @@ -190,39 +169,23 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): show_mention=prefs["bul_show_mention"], ) - if dpv: - I["decision_sem"] = dpv["decisions"][0]["decision_sem"] - else: - I["decision_sem"] = "" I.update(infos) I["etud_etat_html"] = _get_etud_etat_html( formsemestre.etuds_inscriptions[etudid].etat ) I["etud_etat"] = nt.get_etud_etat(etudid) - I["filigranne"] = "" + I["filigranne"] = sco_bulletins_pdf.get_filigranne( + I["etud_etat"], prefs, decision_sem=I["decision_sem"] + ) I["demission"] = "" - if I["etud_etat"] == "D": + if I["etud_etat"] == scu.DEMISSION: I["demission"] = "(Démission)" - I["filigranne"] = "Démission" elif I["etud_etat"] == sco_codes_parcours.DEF: I["demission"] = "(Défaillant)" - I["filigranne"] = "Défaillant" - elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[ - "bul_show_temporary_forced" - ]: - I["filigranne"] = prefs["bul_temporary_txt"] # --- Appreciations - cnx = ndb.GetDBConnexion() - apprecs = sco_etud.appreciations_list( - cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} - ) - I["appreciations_list"] = apprecs - I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs] - I["appreciations"] = I[ - "appreciations_txt" - ] # deprecated / keep it for backward compat in templates + I.update(get_appreciations_list(formsemestre_id, etudid)) # --- Notes ues = nt.get_ues_stat_dict() @@ -316,7 +279,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): else: u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs" else: - u["cur_moy_ue_txt"] = "bonus de %.3g points" % x + u["cur_moy_ue_txt"] = f"bonus de {fmt_note(x)} points" if nt.bonus_ues is not None: u["cur_moy_ue_txt"] += " (+ues)" u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) @@ -407,13 +370,28 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid)) # - C = make_context_dict(I["sem"], I["etud"]) + C = make_context_dict(formsemestre, I["etud"]) C.update(I) # # log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo return C +def get_appreciations_list(formsemestre_id: int, etudid: int) -> dict: + """Appréciations pour cet étudiant dans ce semestre""" + cnx = ndb.GetDBConnexion() + apprecs = sco_etud.appreciations_list( + cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} + ) + d = { + "appreciations_list": apprecs, + "appreciations_txt": [x["date"] + ": " + x["comment"] for x in apprecs], + } + # deprecated / keep it for backward compat in templates: + d["appreciations"] = d["appreciations_txt"] + return d + + def _get_etud_etat_html(etat: str) -> str: """chaine html représentant l'état (backward compat sco7)""" if etat == scu.INSCRIT: # "I" @@ -691,6 +669,7 @@ def etud_descr_situation_semestre( descr_defaillance : "Défaillant" ou vide si non défaillant. decision_jury : "Validé", "Ajourné", ... (code semestre) descr_decision_jury : "Décision jury: Validé" (une phrase) + decision_sem : decisions_ue : noms (acronymes) des UE validées, séparées par des virgules. descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention @@ -700,7 +679,7 @@ def etud_descr_situation_semestre( # --- Situation et décisions jury - # demission/inscription ? + # démission/inscription ? events = sco_etud.scolar_events_list( cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} ) @@ -767,11 +746,15 @@ def etud_descr_situation_semestre( infos["situation"] += " " + infos["descr_defaillance"] dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid]) + if dpv: + infos["decision_sem"] = dpv["decisions"][0]["decision_sem"] + else: + infos["decision_sem"] = "" if not show_decisions: return infos, dpv - # Decisions de jury: + # Décisions de jury: pv = dpv["decisions"][0] dec = "" if pv["decision_sem_descr"]: @@ -810,24 +793,21 @@ def etud_descr_situation_semestre( def formsemestre_bulletinetud( etudid=None, formsemestre_id=None, - format="html", + format=None, version="long", xml_with_decisions=False, force_publishing=False, # force publication meme si semestre non publie sur "portail" prefer_mail_perso=False, ): "page bulletin de notes" - try: - etud = sco_etud.get_etud_info(filled=True)[0] - etudid = etud["etudid"] - except: - sco_etud.log_unknown_etud() - raise ScoValueError("étudiant inconnu") - # API, donc erreurs admises en ScoValueError - sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) + format = format or "html" + etud: Identite = Identite.query.get_or_404(etudid) + formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) + if not formsemestre: + raise ScoValueError(f"semestre {formsemestre_id} inconnu !") bulletin = do_formsemestre_bulletinetud( - formsemestre_id, + formsemestre, etudid, format=format, version=version, @@ -836,52 +816,22 @@ def formsemestre_bulletinetud( prefer_mail_perso=prefer_mail_perso, )[0] if format not in {"html", "pdfmail"}: - filename = scu.bul_filename(sem, etud, format) + filename = scu.bul_filename(formsemestre, etud, format) return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0]) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) H = [ - _formsemestre_bulletinetud_header_html( - etud, etudid, sem, formsemestre_id, format, version - ), + _formsemestre_bulletinetud_header_html(etud, formsemestre, format, version), bulletin, + render_template( + "bul_foot.html", + etud=etud, + formsemestre=formsemestre, + inscription_courante=etud.inscription_courante(), + inscription_str=etud.inscription_descr()["inscription_str"], + ), + html_sco_header.sco_footer(), ] - H.append("""

Situation actuelle: """) - if etud["inscription_formsemestre_id"]: - H.append( - f"""""" - ) - H.append(etud["inscriptionstr"]) - if etud["inscription_formsemestre_id"]: - H.append("""""") - H.append("""

""") - if sem["modalite"] == "EXT": - H.append( - """

- Editer les validations d'UE dans ce semestre extérieur -

""" - % (formsemestre_id, etudid) - ) - # Place du diagramme radar - H.append( - """
- - -
""" - % (etudid, formsemestre_id) - ) - H.append('
') - - # --- Pied de page - H.append(html_sco_header.sco_footer()) - return "".join(H) @@ -896,23 +846,24 @@ def can_send_bulletin_by_mail(formsemestre_id): def do_formsemestre_bulletinetud( - formsemestre_id, - etudid, + formsemestre: FormSemestre, + etudid: int, version="long", # short, long, selectedevals - format="html", + format=None, nohtml=False, - xml_with_decisions=False, # force decisions dans XML - force_publishing=False, # force publication meme si semestre non publie sur "portail" - prefer_mail_perso=False, # mails envoyes sur adresse perso si non vide + xml_with_decisions=False, # force décisions dans XML + force_publishing=False, # force publication meme si semestre non publié sur "portail" + prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide ): """Génère le bulletin au format demandé. Retourne: (bul, filigranne) où bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) et filigranne est un message à placer en "filigranne" (eg "Provisoire"). """ + format = format or "html" if format == "xml": bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud( - formsemestre_id, + formsemestre.id, etudid, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, @@ -923,7 +874,7 @@ def do_formsemestre_bulletinetud( elif format == "json": bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( - formsemestre_id, + formsemestre.id, etudid, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, @@ -931,8 +882,13 @@ def do_formsemestre_bulletinetud( ) return bul, "" - I = formsemestre_bulletinetud_dict(formsemestre_id, etudid) - etud = I["etud"] + if formsemestre.formation.is_apc(): + etud = Identite.query.get(etudid) + r = bulletin_but.BulletinBUT(formsemestre) + I = r.bulletin_etud_complet(etud) + else: + I = formsemestre_bulletinetud_dict(formsemestre.id, etudid) + etud = I["etud"] if format == "html": htm, _ = sco_bulletins_generator.make_formsemestre_bulletinetud( @@ -958,7 +914,7 @@ def do_formsemestre_bulletinetud( elif format == "pdfmail": # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html # check permission - if not can_send_bulletin_by_mail(formsemestre_id): + if not can_send_bulletin_by_mail(formsemestre.id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") if nohtml: @@ -987,7 +943,7 @@ def do_formsemestre_bulletinetud( ) + htm return h, I["filigranne"] # - mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr) + mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr) emaillink = '%s' % ( recipient_addr, recipient_addr, @@ -1055,17 +1011,15 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): ) -def _formsemestre_bulletinetud_header_html( - etud, - etudid, - sem, - formsemestre_id=None, +def _formsemestre_bulletinetud_header_html_old_XXX( + etud: Identite, + formsemestre: FormSemestre, format=None, version=None, ): H = [ html_sco_header.sco_header( - page_title="Bulletin de %(nomprenom)s" % etud, + page_title=f"Bulletin de {etud.nomprenom}", javascripts=[ "js/bulletin.js", "libjs/d3.v3.min.js", @@ -1073,33 +1027,27 @@ def _formsemestre_bulletinetud_header_html( ], cssstyles=["css/radar_bulletin.css"], ), - """
-

%s

- """ - % ( + f"""
+

""" - % request.base_url, - f"""Bulletin {sem["titremois"]} -
""" - % sem, - """""", - """""" % time.strftime("%d/%m/%Y à %Hh%M"), - """
établi le %s (notes sur 20) - """ - % formsemestre_id, - """""" % etudid, - """""" % format, - """ + + """) # Menu endpoint = "notes.formsemestre_bulletinetud" - - menuBul = [ - { - "title": "Réglages bulletins", - "endpoint": "notes.formsemestre_edit_options", - "args": { - "formsemestre_id": formsemestre_id, - # "target_url": url_for( - # "notes.formsemestre_bulletinetud", - # scodoc_dept=g.scodoc_dept, - # formsemestre_id=formsemestre_id, - # etudid=etudid, - # ), - }, - "enabled": (current_user.id in sem["responsables"]) - or current_user.has_permission(Permission.ScoImplement), - }, - { - "title": 'Version papier (pdf, format "%s")' - % sco_bulletins_generator.bulletin_get_class_name_displayed( - formsemestre_id - ), - "endpoint": endpoint, - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "version": version, - "format": "pdf", - }, - }, - { - "title": "Envoi par mail à %s" % etud["email"], - "endpoint": endpoint, - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "version": version, - "format": "pdfmail", - }, - # possible slt si on a un mail... - "enabled": etud["email"] and can_send_bulletin_by_mail(formsemestre_id), - }, - { - "title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"], - "endpoint": endpoint, - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "version": version, - "format": "pdfmail", - "prefer_mail_perso": 1, - }, - # possible slt si on a un mail... - "enabled": etud["emailperso"] - and can_send_bulletin_by_mail(formsemestre_id), - }, - { - "title": "Version json", - "endpoint": endpoint, - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "version": version, - "format": "json", - }, - }, - { - "title": "Version XML", - "endpoint": endpoint, - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "version": version, - "format": "xml", - }, - }, - { - "title": "Ajouter une appréciation", - "endpoint": "notes.appreciation_add_form", - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - }, - "enabled": ( - (current_user.id in sem["responsables"]) - or (current_user.has_permission(Permission.ScoEtudInscrit)) - ), - }, - { - "title": "Enregistrer un semestre effectué ailleurs", - "endpoint": "notes.formsemestre_ext_create_form", - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - }, - "enabled": current_user.has_permission(Permission.ScoImplement), - }, - { - "title": "Enregistrer une validation d'UE antérieure", - "endpoint": "notes.formsemestre_validate_previous_ue", - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - }, - "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), - }, - { - "title": "Enregistrer note d'une UE externe", - "endpoint": "notes.external_ue_create_form", - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - }, - "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), - }, - { - "title": "Entrer décisions jury", - "endpoint": "notes.formsemestre_validation_etud_form", - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - }, - "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), - }, - { - "title": "Editer PV jury", - "endpoint": "notes.formsemestre_pvjury_pdf", - "args": { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - }, - "enabled": True, - }, - ] + menu_autres_operations = make_menu_autres_operations( + formsemestre, etud, endpoint, version + ) H.append("""""") H.append( '' @@ -1258,8 +1075,8 @@ def _formsemestre_bulletinetud_header_html( url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - etudid=etudid, + formsemestre_id=formsemestre.id, + etudid=etud.id, format="pdf", version=version, ), @@ -1272,8 +1089,8 @@ def _formsemestre_bulletinetud_header_html( """
établi le {time.strftime("%d/%m/%Y à %Hh%M")} (notes sur 20) + + + +
""") - H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True)) + H.append(menu_autres_operations) H.append("""
%s%s """ % ( - url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), - sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]), + url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id), + sco_photos.etud_photo_html(etud, title="fiche de " + etud.nomprenom), ) ) H.append( @@ -1283,3 +1100,177 @@ def _formsemestre_bulletinetud_header_html( ) return "".join(H) + + +def make_menu_autres_operations( + formsemestre: FormSemestre, etud: Identite, endpoint: str, version: str +) -> str: + etud_email = etud.get_first_email() or "" + etud_perso = etud.get_first_email("emailperso") or "" + menu_items = [ + { + "title": "Réglages bulletins", + "endpoint": "notes.formsemestre_edit_options", + "args": { + "formsemestre_id": formsemestre.id, + # "target_url": url_for( + # "notes.formsemestre_bulletinetud", + # scodoc_dept=g.scodoc_dept, + # formsemestre_id=formsemestre_id, + # etudid=etudid, + # ), + }, + "enabled": formsemestre.can_be_edited_by(current_user), + }, + { + "title": 'Version papier (pdf, format "%s")' + % sco_bulletins_generator.bulletin_get_class_name_displayed( + formsemestre.id + ), + "endpoint": endpoint, + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + "version": version, + "format": "pdf", + }, + }, + { + "title": f"Envoi par mail à {etud_email}", + "endpoint": endpoint, + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + "version": version, + "format": "pdfmail", + }, + # possible slt si on a un mail... + "enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id), + }, + { + "title": f"Envoi par mail à {etud_perso} (adr. personnelle)", + "endpoint": endpoint, + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + "version": version, + "format": "pdfmail", + "prefer_mail_perso": 1, + }, + # possible slt si on a un mail... + "enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id), + }, + { + "title": "Version json", + "endpoint": endpoint, + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + "version": version, + "format": "json", + }, + }, + { + "title": "Version XML", + "endpoint": endpoint, + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + "version": version, + "format": "xml", + }, + }, + { + "title": "Ajouter une appréciation", + "endpoint": "notes.appreciation_add_form", + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + }, + "enabled": ( + formsemestre.can_be_edited_by(current_user) + or current_user.has_permission(Permission.ScoEtudInscrit) + ), + }, + { + "title": "Enregistrer un semestre effectué ailleurs", + "endpoint": "notes.formsemestre_ext_create_form", + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + }, + "enabled": current_user.has_permission(Permission.ScoImplement), + }, + { + "title": "Enregistrer une validation d'UE antérieure", + "endpoint": "notes.formsemestre_validate_previous_ue", + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + }, + "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), + }, + { + "title": "Enregistrer note d'une UE externe", + "endpoint": "notes.external_ue_create_form", + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + }, + "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), + }, + { + "title": "Entrer décisions jury", + "endpoint": "notes.formsemestre_validation_etud_form", + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + }, + "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), + }, + { + "title": "Éditer PV jury", + "endpoint": "notes.formsemestre_pvjury_pdf", + "args": { + "formsemestre_id": formsemestre.id, + "etudid": etud.id, + }, + "enabled": True, + }, + ] + return htmlutils.make_menu("Autres opérations", menu_items, alone=True) + + +def _formsemestre_bulletinetud_header_html( + etud, + formsemestre: FormSemestre, + format=None, + version=None, +): + H = [ + html_sco_header.sco_header( + page_title=f"Bulletin de {etud.nomprenom}", + javascripts=[ + "js/bulletin.js", + "libjs/d3.v3.min.js", + "js/radar_bulletin.js", + ], + cssstyles=["css/radar_bulletin.css"], + ), + render_template( + "bul_head.html", + etud=etud, + format=format, + formsemestre=formsemestre, + menu_autres_operations=make_menu_autres_operations( + etud=etud, + formsemestre=formsemestre, + endpoint="notes.formsemestre_bulletinetud", + version=version, + ), + scu=scu, + time=time, + version=version, + ), + ] + return "\n".join(H) diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index 04a9efaee..2aeb792fb 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -63,48 +63,14 @@ from app.scodoc import sco_pdf from app.scodoc.sco_pdf import PDFLOCK import sco_version -# Liste des types des classes de générateurs de bulletins PDF: -BULLETIN_CLASSES = collections.OrderedDict() - -def register_bulletin_class(klass): - BULLETIN_CLASSES[klass.__name__] = klass - - -def bulletin_class_descriptions(): - return [x.description for x in BULLETIN_CLASSES.values()] - - -def bulletin_class_names(): - return list(BULLETIN_CLASSES.keys()) - - -def bulletin_default_class_name(): - return bulletin_class_names()[0] - - -def bulletin_get_class(class_name): - return BULLETIN_CLASSES[class_name] - - -def bulletin_get_class_name_displayed(formsemestre_id): - """Le nom du générateur utilisé, en clair""" - from app.scodoc import sco_preferences - - bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) - try: - gen_class = bulletin_get_class(bul_class_name) - return gen_class.description - except: - return "invalide ! (voir paramètres)" - - -class BulletinGenerator(object): +class BulletinGenerator: "Virtual superclass for PDF bulletin generators" "" # Here some helper methods # see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ] description = "superclass for bulletins" # description for user interface + list_in_menu = True # la classe doit-elle est montrée dans le menu de config ? def __init__( self, @@ -151,7 +117,7 @@ class BulletinGenerator(object): def get_filename(self): """Build a filename to be proposed to the web client""" sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"]) - return scu.bul_filename(sem, self.infos["etud"], "pdf") + return scu.bul_filename_old(sem, self.infos["etud"], "pdf") def generate(self, format="", stand_alone=True): """Return bulletin in specified format""" @@ -270,9 +236,14 @@ def make_formsemestre_bulletinetud( formsemestre_id = infos["formsemestre_id"] bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) - try: + + gen_class = None + if infos.get("type") == "BUT" and format.startswith("pdf"): + gen_class = bulletin_get_class(bul_class_name + "BUT") + if gen_class is None: gen_class = bulletin_get_class(bul_class_name) - except: + + if gen_class is None: raise ValueError( "Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name ) @@ -313,3 +284,52 @@ def make_formsemestre_bulletinetud( filename = bul_generator.get_filename() return data, filename + + +#### + +# Liste des types des classes de générateurs de bulletins PDF: +BULLETIN_CLASSES = collections.OrderedDict() + + +def register_bulletin_class(klass): + BULLETIN_CLASSES[klass.__name__] = klass + + +def bulletin_class_descriptions(): + return [ + BULLETIN_CLASSES[class_name].description + for class_name in BULLETIN_CLASSES + if BULLETIN_CLASSES[class_name].list_in_menu + ] + + +def bulletin_class_names() -> list[str]: + "Liste les noms des classes de bulletins à présenter à l'utilisateur" + return [ + class_name + for class_name in BULLETIN_CLASSES + if BULLETIN_CLASSES[class_name].list_in_menu + ] + + +def bulletin_default_class_name(): + return bulletin_class_names()[0] + + +def bulletin_get_class(class_name: str) -> BulletinGenerator: + """La class de génération de bulletin de ce nom, + ou None si pas trouvée + """ + return BULLETIN_CLASSES.get(class_name) + + +def bulletin_get_class_name_displayed(formsemestre_id): + """Le nom du générateur utilisé, en clair""" + from app.scodoc import sco_preferences + + bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) + gen_class = bulletin_get_class(bul_class_name) + if gen_class is None: + return "invalide ! (voir paramètres)" + return gen_class.description diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index ee57b60e7..ee0ddae27 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -138,7 +138,7 @@ def formsemestre_bulletinetud_published_dict( if not published: return d # stop ! - etat_inscription = etud.etat_inscription(formsemestre.id) + etat_inscription = etud.inscription_etat(formsemestre.id) if etat_inscription != scu.INSCRIT: d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True)) return d diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 94cfcf6bc..1df2ca666 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -61,12 +61,10 @@ from reportlab.platypus.doctemplate import BaseDocTemplate from flask import g, request from app import log, ScoValueError -from app.comp import res_sem -from app.comp.res_common import NotesTableCompat from app.models import FormSemestre from app.scodoc import sco_cache -from app.scodoc import sco_formsemestre +from app.scodoc import sco_codes_parcours from app.scodoc import sco_pdf from app.scodoc import sco_preferences from app.scodoc import sco_etud @@ -190,7 +188,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"): i = 1 for etud in formsemestre.get_inscrits(include_demdef=True, order=True): frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( - formsemestre_id, + formsemestre, etud.id, format="pdfpart", version=version, @@ -239,8 +237,9 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): filigrannes = {} i = 1 for sem in etud["sems"]: + formsemestre = FormSemestre.query.get(sem["formsemestre_id"]) frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( - sem["formsemestre_id"], + formsemestre, etudid, format="pdfpart", version=version, @@ -275,3 +274,16 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): ) return pdfdoc, filename + + +def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str: + """Texte à placer en "filigranne" sur le bulletin pdf""" + if etud_etat == scu.DEMISSION: + return "Démission" + elif etud_etat == sco_codes_parcours.DEF: + return "Défaillant" + elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[ + "bul_show_temporary_forced" + ]: + return prefs["bul_temporary_txt"] + return "" diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index 25111f7e6..fd84e7d94 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -66,7 +66,8 @@ from app.scodoc import sco_groups from app.scodoc import sco_evaluations from app.scodoc import gen_tables -# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences) +# Important: Le nom de la classe ne doit pas changer (bien le choisir), +# car il sera stocké en base de données (dans les préférences) class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc supported_formats = ["html", "pdf"] @@ -264,11 +265,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): def build_bulletin_table(self): """Génère la table centrale du bulletin de notes - Renvoie: colkeys, P, pdf_style, colWidths - - colkeys: nom des colonnes de la table (clés) - - table (liste de dicts de chaines de caracteres) - - style (commandes table Platypus) - - largeurs de colonnes pour PDF + Renvoie: col_keys, P, pdf_style, col_widths + - col_keys: nom des colonnes de la table (clés) + - table: liste de dicts de chaines de caractères + - pdf_style: commandes table Platypus + - col_widths: largeurs de colonnes pour PDF """ I = self.infos P = [] # elems pour générer table avec gen_table (liste de dicts) @@ -287,25 +288,25 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"] with_col_ects = prefs["bul_show_ects"] - colkeys = ["titre", "module"] # noms des colonnes à afficher + col_keys = ["titre", "module"] # noms des colonnes à afficher if with_col_rang: - colkeys += ["rang"] + col_keys += ["rang"] if with_col_minmax: - colkeys += ["min"] + col_keys += ["min"] if with_col_moypromo: - colkeys += ["moy"] + col_keys += ["moy"] if with_col_minmax: - colkeys += ["max"] - colkeys += ["note"] + col_keys += ["max"] + col_keys += ["note"] if with_col_coef: - colkeys += ["coef"] + col_keys += ["coef"] if with_col_ects: - colkeys += ["ects"] + col_keys += ["ects"] if with_col_abs: - colkeys += ["abs"] + col_keys += ["abs"] colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus) i = 0 - for k in colkeys: + for k in col_keys: colidx[k] = i i += 1 @@ -313,7 +314,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm else: bul_pdf_mod_colwidth = None - colWidths = { + col_widths = { "titre": None, "module": bul_pdf_mod_colwidth, "min": 1.5 * cm, @@ -541,7 +542,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): ("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu: ] # - return colkeys, P, pdf_style, colWidths + return col_keys, P, pdf_style, col_widths def _list_modules( self, diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 63bcb72ab..3068ee397 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -512,8 +512,8 @@ def module_edit(module_id=None): ] else: mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] - ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] + ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) semestres_indices = list(range(1, parcours.NB_SEM + 1)) @@ -748,8 +748,11 @@ def module_edit(module_id=None): else: # l'UE de rattachement peut changer tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") + x, y = tf[2]["ue_matiere_id"].split("!") + tf[2]["ue_id"] = int(x) + tf[2]["matiere_id"] = int(y) old_ue_id = a_module.ue.id - new_ue_id = int(tf[2]["ue_id"]) + new_ue_id = tf[2]["ue_id"] if (old_ue_id != new_ue_id) and in_use: new_ue = UniteEns.query.get_or_404(new_ue_id) if new_ue.semestre_idx != a_module.ue.semestre_idx: diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 14e4faec4..8f18a5b2c 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -954,13 +954,13 @@ def _ue_table_ues( if cur_ue_semestre_id != ue["semestre_id"]: cur_ue_semestre_id = ue["semestre_id"] - # if iue > 0: - # H.append("") if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: lab = "Pas d'indication de semestre:" else: lab = "Semestre %s:" % ue["semestre_id"] - H.append('
%s
' % lab) + H.append( + '
%s
' % lab + ) H.append('
    ') H.append('
  • ') if iue != 0 and editable: @@ -1028,7 +1028,9 @@ def _ue_table_ues( H.append( f"""
""" + }">Ajouter une UE dans le semestre {ue['semestre_id'] or ''} +
+ """ ) iue += 1 diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index d019d2df2..48c6b82bf 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -33,8 +33,7 @@ import os import time from operator import itemgetter -from flask import url_for, g, request -from flask_mail import Message +from flask import url_for, g from app import email from app import log @@ -46,7 +45,6 @@ from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc import safehtml from app.scodoc import sco_preferences from app.scodoc.scolog import logdb -from app.scodoc.TrivialFormulator import TrivialFormulator def format_etud_ident(etud): @@ -860,7 +858,7 @@ def list_scolog(etudid): return cursor.dictfetchall() -def fill_etuds_info(etuds, add_admission=True): +def fill_etuds_info(etuds: list[dict], add_admission=True): """etuds est une liste d'etudiants (mappings) Pour chaque etudiant, ajoute ou formatte les champs -> informations pour fiche etudiant ou listes diverses @@ -977,7 +975,10 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict: def descr_situation_etud(etudid: int, ne="") -> str: - """chaîne décrivant la situation actuelle de l'étudiant""" + """Chaîne décrivant la situation actuelle de l'étudiant + XXX Obsolete, utiliser Identite.descr_situation_etud() dans + les nouveaux codes + """ from app.scodoc import sco_formsemestre cnx = ndb.GetDBConnexion() diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 4bc039baa..9c5630480 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -180,7 +180,9 @@ def search_etud_in_dept(expnom=""): e["_nomprenom_target"] = target e["inscription_target"] = target e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"]) - sco_groups.etud_add_group_infos(e, e["cursem"]) + sco_groups.etud_add_group_infos( + e, e["cursem"]["formsemestre_id"] if e["cursem"] else None + ) tab = GenTable( columns_ids=("nomprenom", "code_nip", "inscription", "groupes"), diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 2080e9572..11e665d92 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -31,7 +31,7 @@ from flask import current_app from flask import g from flask import request -from flask import url_for +from flask import render_template, url_for from flask_login import current_user from app import log @@ -411,7 +411,7 @@ def formsemestre_status_menubar(sem): "enabled": sco_permissions_check.can_validate_sem(formsemestre_id), }, { - "title": "Editer les PV et archiver les résultats", + "title": "Éditer les PV et archiver les résultats", "endpoint": "notes.formsemestre_archive", "args": {"formsemestre_id": formsemestre_id}, "enabled": sco_permissions_check.can_edit_pv(formsemestre_id), @@ -445,6 +445,7 @@ def retreive_formsemestre_from_request() -> int: """Cherche si on a de quoi déduire le semestre affiché à partir des arguments de la requête: formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id + Returns None si pas défini. """ if request.method == "GET": args = request.args @@ -505,34 +506,17 @@ def formsemestre_page_title(): return "" try: formsemestre_id = int(formsemestre_id) - sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy() + formsemestre = FormSemestre.query.get(formsemestre_id) except: log("can't find formsemestre_id %s" % formsemestre_id) return "" - fill_formsemestre(sem) - - h = f"""
- - {formsemestre_status_menubar(sem)} -
- """ + h = render_template( + "formsemestre_page_title.html", + formsemestre=formsemestre, + scu=scu, + sem_menu_bar=formsemestre_status_menubar(formsemestre.to_dict()), + ) return h diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 6a1ee4679..2a27b498d 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -321,7 +321,7 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud t["etath"] = t["etat"] # Add membership for all partitions, 'partition_id' : group for etud in members: # long: comment eviter ces boucles ? - etud_add_group_infos(etud, sem) + etud_add_group_infos(etud, sem["formsemestre_id"]) if group["group_name"] != None: group_tit = "%s %s" % (group["partition_name"], group["group_name"]) @@ -413,12 +413,12 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"): return R -def etud_add_group_infos(etud, sem, sep=" "): +def etud_add_group_infos(etud, formsemestre_id, sep=" "): """Add informations on partitions and group memberships to etud (a dict with an etudid)""" etud[ "partitions" ] = collections.OrderedDict() # partition_id : group + partition_name - if not sem: + if not formsemestre_id: etud["groupes"] = "" return etud @@ -430,7 +430,7 @@ def etud_add_group_infos(etud, sem, sep=" "): and p.formsemestre_id = %(formsemestre_id)s ORDER BY p.numero """, - {"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]}, + {"etudid": etud["etudid"], "formsemestre_id": formsemestre_id}, ) for info in infos: @@ -439,13 +439,13 @@ def etud_add_group_infos(etud, sem, sep=" "): # resume textuel des groupes: etud["groupes"] = sep.join( - [g["group_name"] for g in infos if g["group_name"] != None] + [gr["group_name"] for gr in infos if gr["group_name"] is not None] ) etud["partitionsgroupes"] = sep.join( [ - g["partition_name"] + ":" + g["group_name"] - for g in infos - if g["group_name"] != None + gr["partition_name"] + ":" + gr["group_name"] + for gr in infos + if gr["group_name"] is not None ] ) diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index b717f2d19..6d4988d17 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -203,7 +203,7 @@ def sco_import_generate_excel_sample( for field in titles: if field == "groupes": sco_groups.etud_add_group_infos( - etud, groups_infos.formsemestre, sep=";" + etud, groups_infos.formsemestre_id, sep=";" ) l.append(etud["partitionsgroupes"]) else: diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 831cc87a9..807792fb0 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -196,7 +196,10 @@ def do_inscrit(sem, etudids, inscrit_groupes=False): if len(etud["sems"]) < 2: continue prev_formsemestre = etud["sems"][1] - sco_groups.etud_add_group_infos(etud, prev_formsemestre) + sco_groups.etud_add_group_infos( + etud, + prev_formsemestre["formsemestre_id"] if prev_formsemestre else None, + ) cursem_groups_by_name = dict( [ diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 358521397..73a355b62 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -215,7 +215,9 @@ def ficheEtud(etudid=None): info["modifadresse"] = "" # Groupes: - sco_groups.etud_add_group_infos(info, info["cursem"]) + sco_groups.etud_add_group_infos( + info, info["cursem"]["formsemestre_id"] if info["cursem"] else None + ) # Parcours de l'étudiant if info["sems"]: diff --git a/app/scodoc/sco_photos.py b/app/scodoc/sco_photos.py index 80f3553e5..0dfeaafe3 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -175,7 +175,7 @@ def etud_photo_is_local(etud: dict, size="small"): return photo_pathname(etud["photo_filename"], size=size) -def etud_photo_html(etud=None, etudid=None, title=None, size="small"): +def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"): """HTML img tag for the photo, either in small size (h90) or original size (size=="orig") """ @@ -351,7 +351,8 @@ def copy_portal_photo_to_fs(etud): """Copy the photo from portal (distant website) to local fs. Returns rel. path or None if copy failed, with a diagnostic message """ - sco_etud.format_etud_ident(etud) + if "nomprenom" not in etud: + sco_etud.format_etud_ident(etud) url = photo_portal_url(etud) if not url: return None, "%(nomprenom)s: pas de code NIP" % etud diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 7476be6b9..b320c164e 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -2138,7 +2138,7 @@ class BasePreferences(object): return form -class SemPreferences(object): +class SemPreferences: """Preferences for a formsemestre""" def __init__(self, formsemestre_id=None): @@ -2294,9 +2294,8 @@ def doc_preferences(): return "\n".join([" | ".join(x) for x in L]) -def bulletin_option_affichage(formsemestre_id: int) -> dict: +def bulletin_option_affichage(formsemestre_id: int, prefs: SemPreferences) -> dict: "dict avec les options d'affichages (préférences) pour ce semestre" - prefs = SemPreferences(formsemestre_id) fields = ( "bul_show_abs", "bul_show_abs_modules", diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 196c2c210..ba7fd504a 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -608,7 +608,7 @@ def is_valid_filename(filename): return VALID_EXP.match(filename) -def bul_filename(sem, etud, format): +def bul_filename_old(sem: dict, etud: dict, format): """Build a filename for this bulletin""" dt = time.strftime("%Y-%m-%d") filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}" @@ -616,6 +616,14 @@ def bul_filename(sem, etud, format): return filename +def bul_filename(formsemestre, etud, format): + """Build a filename for this bulletin""" + dt = time.strftime("%Y-%m-%d") + filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}.{format}" + filename = make_filename(filename) + return filename + + def flash_errors(form): """Flashes form errors (version sommaire)""" for field, errors in form.errors.items(): diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index a20c8bfa3..d9756ace4 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -14,16 +14,25 @@ } main{ --couleurPrincipale: rgb(240,250,255); - --couleurFondTitresUE: rgb(206,255,235); - --couleurFondTitresRes: rgb(125, 170, 255); - --couleurFondTitresSAE: rgb(211, 255, 255); + --couleurFondTitresUE: #b6ebff; + --couleurFondTitresRes: #f8c844; + --couleurFondTitresSAE: #c6ffab; --couleurSecondaire: #fec; - --couleurIntense: #c09; - --couleurSurlignage: rgba(232, 255, 132, 0.47); + --couleurIntense: rgb(4, 16, 159);; + --couleurSurlignage: rgba(255, 253, 110, 0.49); max-width: 1000px; margin: auto; display: none; } +.releve a, .releve a:visited { + color: navy; + text-decoration: none; +} +.releve a:hover { + color: red; + text-decoration: underline; +} + .ready .wait{display: none;} .ready main{display: block;} h2{ @@ -97,7 +106,8 @@ section>div:nth-child(1){ .hide_coef .synthese em, .hide_coef .eval>em, .hide_date_inscr .dateInscription, -.hide_ects .ects{ +.hide_ects .ects, +.hide_rangs .rang{ display: none; } @@ -151,14 +161,19 @@ section>div:nth-child(1){ column-gap: 4px; flex: none; } -.infoSemestre>div:nth-child(1){ - margin-right: auto; -} + .infoSemestre>div>div:nth-child(even){ text-align: right; } +.photo { + border: none; + margin-left: auto; +} .rang{ - text-decoration: underline var(--couleurIntense); + font-weight: bold; +} +.ue .rang{ + font-weight: 400; } .decision{ margin: 5px 0; @@ -186,6 +201,9 @@ section>div:nth-child(1){ .synthese h3{ background: var(--couleurFondTitresUE); } +.synthese .ue>div{ + text-align: right; +} .synthese em, .eval em{ opacity: 0.6; @@ -206,7 +224,6 @@ section>div:nth-child(1){ scroll-margin-top: 60px; } .module, .ue { - background: var(--couleurSecondaire); color: #000; padding: 4px 32px; border-radius: 4px; @@ -218,6 +235,15 @@ section>div:nth-child(1){ cursor: pointer; position: relative; } +.ue { + background: var(--couleurFondTitresRes); +} +.module { + background: var(--couleurFondTitresRes); +} +.module h3 { + background: var(--couleurFondTitresRes); +} .module::before, .ue::before { content:url("data:image/svg+xml;utf8,"); width: 26px; @@ -308,6 +334,14 @@ h3{ margin-bottom: 8px; } +@media screen and (max-width: 700px) { + section{ + padding: 16px; + } + .syntheseModule, .eval { + margin: 0; + } +} /*.absences{ display: grid; grid-template-columns: auto auto; diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index d735077a4..b5f2b325b 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1702,7 +1702,7 @@ ul.notes_ue_list { margin-top: 4px; margin-right: 1em; margin-left: 1em; - padding-top: 1em; + /* padding-top: 1em; */ padding-bottom: 1em; font-weight: bold; } @@ -1767,9 +1767,25 @@ ul.notes_module_list { font-style: normal; } +div.ue_list_div { + border: 3px solid rgb(35, 0, 160); + padding-left: 5px; + padding-top: 5px; + margin-bottom: 5px; + margin-right: 5px; +} + div.ue_list_tit_sem { font-size: 120%; font-weight: bold; + color: orangered; + display: list-item; /* This has to be "list-item" */ + list-style-type: disc; /* See https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type */ + list-style-position: inside; +} + +input.sco_tag_checkbox { + margin-bottom: 10px; } .notes_ue_list a.stdlink { @@ -1947,7 +1963,20 @@ table.notes_recapcomplet a:hover { div.notes_bulletin { margin-right: 5px; } - +div.bull_head { + display: grid; + justify-content: space-between; + grid-template-columns: auto auto; +} +div.bull_photo { + display: inline-block; + margin-right: 10px; +} +span.bulletin_menubar_but { + display: inline-block; + margin-left: 2em; + margin-right: 2em; +} table.notes_bulletin { border-collapse: collapse; border: 2px solid rgb(100,100,240); @@ -2087,12 +2116,6 @@ a.bull_link:hover { text-decoration: underline; } -table.bull_head { - width: 100%; -} -td.bull_photo { - text-align: right; -} div.bulletin_menubar { padding-left: 25px; diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png index 5c710a76a..06746f952 100644 Binary files a/app/static/icons/scologo_img.png and b/app/static/icons/scologo_img.png differ diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index dcc4520bd..97f97e29d 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -41,7 +41,7 @@ class releveBUT extends HTMLElement { } set showData(data) { - this.showInformations(data); + // this.showInformations(data); this.showSemestre(data); this.showSynthese(data); this.showEvaluations(data); @@ -68,13 +68,7 @@ class releveBUT extends HTMLElement {
- - - -
- Photo de l'étudiant -
-
+ @@ -85,8 +79,8 @@ class releveBUT extends HTMLElement {
-

Semestre

-
+

+
@@ -103,7 +97,7 @@ class releveBUT extends HTMLElement {
-

Synthèse

+

Unités d'enseignement

La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.
@@ -132,7 +126,7 @@ class releveBUT extends HTMLElement {
-

SAÉ

+

Situations d'apprentissage et d'évaluation (SAÉ)

Liste @@ -198,7 +192,8 @@ class releveBUT extends HTMLElement { /* Information sur le semestre */ /*******************************/ showSemestre(data) { - this.shadow.querySelector("h2").innerHTML += data.semestre.numero; + + this.shadow.querySelector("#identite_etudiant").innerHTML = ` ${data.etudiant.nomprenom} `; this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription); let output = `
@@ -212,7 +207,9 @@ class releveBUT extends HTMLElement {
Absences
N.J. ${data.semestre.absences?.injustifie ?? "-"}
Total ${data.semestre.absences?.total ?? "-"}
-
`; +
+ photo de l'étudiant + `; /*${data.semestre.groupes.map(groupe => { return `
@@ -254,6 +251,7 @@ class releveBUT extends HTMLElement {
Moyenne : ${dataUE.moyenne?.value || "-"}
+
Rang : ${dataUE.moyenne?.rang} / ${dataUE.moyenne?.total}
Bonus : ${dataUE.bonus || 0} - Malus : ${dataUE.malus || 0} diff --git a/app/templates/bul_foot.html b/app/templates/bul_foot.html new file mode 100644 index 000000000..873b43c76 --- /dev/null +++ b/app/templates/bul_foot.html @@ -0,0 +1,34 @@ +{# -*- mode: jinja-html -*- #} +{# Pied des bulletins HTML #} + +

Situation actuelle: +{% if inscription_courante %} +{{inscription_str}} +{% else %} + {{inscription_str}} +{% endif %} +

+ +{% if formsemestre.modalite == "EXT" %} +

+ Éditer les validations d'UE dans ce semestre extérieur +

+{% endif %} + +{# Place du diagramme radar #} +
+ + +
+
+ + diff --git a/app/templates/bul_head.html b/app/templates/bul_head.html new file mode 100644 index 000000000..d706ef9ae --- /dev/null +++ b/app/templates/bul_head.html @@ -0,0 +1,57 @@ +{# -*- mode: jinja-html -*- #} +{# L'en-tête des bulletins HTML #} +{# was _formsemestre_bulletinetud_header_html #} + +
+
+{% if not is_apc %} +

{{etud.nomprenom}}

+{% endif %} +
+ + + + Bulletin + {{formsemestre.titre_mois() + }} + +
+ établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20) + + + + + {{menu_autres_operations|safe}} + {{scu.ICON_PDF|safe}} + +
+
+
+{% if not is_apc %} + +{% endif %} +
diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index ff0682fce..d394f255b 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -7,8 +7,13 @@ {% block app_content %} +{% include 'bul_head.html' %} + + +{% include 'bul_foot.html' %} +