From 8f911234b209b1e1741c308d3d5f55b808d85e48 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 6 Mar 2022 22:40:20 +0100 Subject: [PATCH] modernisation/methodes sur Identite/bul. head. --- app/but/bulletin_but.py | 2 +- app/but/bulletin_but_xml_compat.py | 2 +- app/models/etudiants.py | 138 +++++++++++++++++- app/models/formsemestre.py | 1 - app/models/moduleimpls.py | 3 +- app/scodoc/sco_bulletins.py | 197 +++++++++++++++----------- app/scodoc/sco_bulletins_generator.py | 2 +- app/scodoc/sco_bulletins_json.py | 2 +- app/scodoc/sco_etud.py | 11 +- app/scodoc/sco_photos.py | 3 +- app/scodoc/sco_utils.py | 10 +- app/static/js/releve-but.js | 10 +- app/templates/bul_head.html | 58 ++++++++ app/templates/but/bulletin.html | 2 + 14 files changed, 334 insertions(+), 107 deletions(-) create mode 100644 app/templates/bul_head.html diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index c0c29bf1..4379fdac 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -224,7 +224,7 @@ class BulletinBUT: (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 = { diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 73e06c4d..bab7b728 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 18f13380..953fb280 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): @@ -172,6 +179,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 +207,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 +219,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 c66df782..ce162d24 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 diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 0aa74ef4..1935036e 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/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 6b1afc4a..046c8146 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -29,13 +29,11 @@ """ import email -import pprint 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 import email from app import log @@ -802,18 +800,10 @@ def formsemestre_bulletinetud( 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") - + etud: Identite = Identite.query.get_or_404(etudid) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) if not formsemestre: - # API, donc erreurs admises raise ScoValueError(f"semestre {formsemestre_id} inconnu !") - sem = formsemestre.to_dict() bulletin = do_formsemestre_bulletinetud( formsemestre, @@ -825,37 +815,39 @@ 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]) H = [ - _formsemestre_bulletinetud_header_html( - etud, etudid, formsemestre, format, version - ), + _formsemestre_bulletinetud_header_html(etud, formsemestre, format, version), bulletin, ] H.append("""

Situation actuelle: """) - if etud["inscription_formsemestre_id"]: + inscription_courante = etud.inscription_courante() + if inscription_courante: H.append( f"""""" ) - H.append(etud["inscriptionstr"]) - if etud["inscription_formsemestre_id"]: + inscription_descr = etud.inscription_descr() + H.append(inscription_descr["inscription_str"]) + if inscription_courante: H.append("""""") H.append("""

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

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

""" - % (formsemestre_id, etudid) ) # Place du diagramme radar H.append( @@ -1048,16 +1040,15 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): ) -def _formsemestre_bulletinetud_header_html( - etud, - etudid, +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", @@ -1068,8 +1059,8 @@ def _formsemestre_bulletinetud_header_html( f"""""") # Menu endpoint = "notes.formsemestre_bulletinetud" + menu_autres_operations = make_menu_autres_operations( + formsemestre, etud, endpoint, version + ) - menuBul = [ + H.append("""""") + H.append( + '' + % ( + url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + etudid=etud.id, + format="pdf", + version=version, + ), + scu.ICON_PDF, + ) + ) + H.append("""

{etud["nomprenom"]}

+ "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id + )}">{etud.nomprenom}
Bulletin
- +
""") + H.append(menu_autres_operations) + H.append("""
%s
""") + # + H.append( + """%s + """ + % ( + 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( + """ + + """ + ) + + 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", @@ -1124,43 +1159,42 @@ def _formsemestre_bulletinetud_header_html( "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "pdf", }, }, { - "title": "Envoi par mail à %s" % etud["email"], + "title": f"Envoi par mail à {etud_email}", "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "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), + "enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id), }, { - "title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"], + "title": f"Envoi par mail à {etud_perso} (adr. personnelle)", "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "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), + "enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id), }, { "title": "Version json", "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "json", }, @@ -1170,7 +1204,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": endpoint, "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, "version": version, "format": "xml", }, @@ -1180,7 +1214,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.appreciation_add_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": ( formsemestre.can_be_edited_by(current_user) @@ -1192,7 +1226,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_ext_create_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": current_user.has_permission(Permission.ScoImplement), }, @@ -1201,7 +1235,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_validate_previous_ue", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, @@ -1210,7 +1244,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.external_ue_create_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, @@ -1219,7 +1253,7 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_validation_etud_form", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": sco_permissions_check.can_validate_sem(formsemestre.id), }, @@ -1228,43 +1262,44 @@ def _formsemestre_bulletinetud_header_html( "endpoint": "notes.formsemestre_pvjury_pdf", "args": { "formsemestre_id": formsemestre.id, - "etudid": etudid, + "etudid": etud.id, }, "enabled": True, }, ] + return htmlutils.make_menu("Autres opérations", menu_items, alone=True) - H.append("""
""") - H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True)) - H.append("""
""") - H.append( - ' %s' - % ( - url_for( - "notes.formsemestre_bulletinetud", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - etudid=etudid, - format="pdf", + +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.ICON_PDF, - ) - ) - H.append("""""") - # - H.append( - """%s - """ - % ( - url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), - sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]), - ) - ) - H.append( - """ - - """ - ) - - return "".join(H) + 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 ceeb0aac..7f6a53c6 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -117,7 +117,7 @@ class BulletinGenerator: 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""" diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index ee57b60e..ee0ddae2 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_etud.py b/app/scodoc/sco_etud.py index d019d2df..48c6b82b 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_photos.py b/app/scodoc/sco_photos.py index 80f3553e..cf66a852 100644 --- a/app/scodoc/sco_photos.py +++ b/app/scodoc/sco_photos.py @@ -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_utils.py b/app/scodoc/sco_utils.py index 196c2c21..b30c493c 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.sem.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/js/releve-but.js b/app/static/js/releve-but.js index 076fef1a..c7c649e5 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 -
-
+ diff --git a/app/templates/bul_head.html b/app/templates/bul_head.html new file mode 100644 index 00000000..5211fcd4 --- /dev/null +++ b/app/templates/bul_head.html @@ -0,0 +1,58 @@ +{# -*- mode: jinja-html -*- #} +{# L'en-tête des bulletins HTML #} +{# was _formsemestre_bulletinetud_header_html #} + + + + + + +
+

{{etud.nomprenom}}

+
+ Bulletin {{formsemestre.titre_mois()}} +
+ + + + + + + +
établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20) + + + + + + +
{{menu_autres_operations|safe}}
+
{{scu.ICON_PDF|safe}} +
+
+
{{etud.photo_html(title="fiche de " + etud["nom"])|safe}} +
diff --git a/app/templates/but/bulletin.html b/app/templates/but/bulletin.html index ff0682fc..02a09e84 100644 --- a/app/templates/but/bulletin.html +++ b/app/templates/but/bulletin.html @@ -6,6 +6,8 @@ {% endblock %} {% block app_content %} +

Totoro

+