# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Emmanuel Viennet emmanuel.viennet@gmail.com # ############################################################################## """Generation du bulletin note au format standard Nouvelle version juillet 2011: changement de la présentation de la table. Note sur le PDF: Les templates utilisent les XML markup tags de ReportLab (voir ReportLab user guide, page 70 et suivantes), dans lesquels les balises de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans: - preferences du semestre (ou globales) (voir sco_preferences.py) - champs de formsemestre: titre, date_debut, date_fin, responsable, anneesem - champs de l'etudiant s(etud, décoré par getEtudInfo) - demission ("DEMISSION" ou vide) - situation ("Inscrit le XXX") Balises img: actuellement interdites. """ from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table from reportlab.lib.units import cm, mm from reportlab.lib.colors import Color, blue from app.models import FormSemestre from app.scodoc.sco_exceptions import ScoBugCatcher import app.scodoc.sco_utils as scu from app.scodoc.sco_pdf import SU from app.scodoc import sco_preferences from app.scodoc.sco_permissions import Permission from app.scodoc.sco_codes_parcours import ( UE_COLORS, UE_DEFAULT_COLOR, UE_ELECTIVE, UE_SPORT, UE_STANDARD, ) from app.scodoc import sco_bulletins_generator from app.scodoc import sco_bulletins_pdf 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) 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"] def bul_title_pdf(self) -> list: """Génère la partie "titre" du bulletin de notes. Renvoie une liste d'objets platypus """ objects = sco_bulletins_pdf.process_field( self.preferences["bul_pdf_title"], self.infos, self.FieldStyle ) objects.append( Spacer(1, 5 * mm) ) # impose un espace vertical entre le titre et la table qui suit return objects 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"] colkeys, P, pdf_style, colWidths = self.build_bulletin_table() T = gen_tables.GenTable( rows=P, columns_ids=colkeys, pdf_table_style=pdf_style, pdf_col_widths=[colWidths[k] for k in colkeys], preferences=sco_preferences.SemPreferences(formsemestre_id), html_class="notes_bulletin", html_class_ignore_default=True, html_with_td_classes=True, ) return T.gen(format=format) def bul_part_below(self, format="html"): """Génère les informations placées sous la table de notes (absences, appréciations, décisions de jury...) Renvoie: - en HTML: une chaine - en PDF: une liste d'objets platypus """ H = [] # html story = [] # objets platypus # ----- ABSENCES if self.preferences["bul_show_abs"]: nbabs = self.infos["nbabs"] story.append(Spacer(1, 2 * mm)) if nbabs: H.append( """<p class="bul_abs"> <a href="../Absences/CalAbs?etudid=%(etudid)s" class="bull_link"> <b>Absences :</b> %(nbabs)s demi-journées, dont %(nbabsjust)s justifiées (pendant ce semestre). </a></p> """ % self.infos ) story.append( Paragraph( SU( "%(nbabs)s absences (1/2 journées), dont %(nbabsjust)s justifiées." % self.infos ), self.CellStyle, ) ) else: H.append("""<p class="bul_abs">Pas d'absences signalées.</p>""") story.append(Paragraph(SU("Pas d'absences signalées."), self.CellStyle)) # ---- APPRECIATIONS # le dir. des etud peut ajouter des appreciations, # mais aussi le chef (perm. ScoEtudInscrit) can_edit_app = (self.authuser.id in self.infos["responsables"]) or ( self.authuser.has_permission(Permission.ScoEtudInscrit) ) H.append('<div class="bull_appreciations">') for app in self.infos["appreciations_list"]: if can_edit_app: mlink = ( '<a class="stdlink" href="appreciation_add_form?id=%s">modifier</a> <a class="stdlink" href="appreciation_add_form?id=%s&suppress=1">supprimer</a>' % (app["id"], app["id"]) ) else: mlink = "" H.append( '<p><span class="bull_appreciations_date">%s</span>%s<span class="bull_appreciations_link">%s</span></p>' % (app["date"], app["comment"], mlink) ) if can_edit_app: H.append( '<p><a class="stdlink" href="appreciation_add_form?etudid=%(etudid)s&formsemestre_id=%(formsemestre_id)s">Ajouter une appréciation</a></p>' % self.infos ) H.append("</div>") # Appréciations sur PDF: if self.infos.get("appreciations_list", False): story.append(Spacer(1, 3 * mm)) story.append( Paragraph( SU("Appréciation : " + "\n".join(self.infos["appreciations_txt"])), self.CellStyle, ) ) # ----- DECISION JURY if self.preferences["bul_show_decision"]: story += sco_bulletins_pdf.process_field( self.preferences["bul_pdf_caption"], self.infos, self.FieldStyle, format="pdf", ) field = sco_bulletins_pdf.process_field( self.preferences["bul_pdf_caption"], self.infos, self.FieldStyle, format="html", ) H.append('<div class="bul_decision">' + field + "</div>") # ----- if format == "pdf": if self.scale_table_in_page: # le scaling (pour tenir sur une page) semble incompatible avec # le KeepTogether() return story else: return [KeepTogether(story)] elif format == "html": return "\n".join(H) def bul_signatures_pdf(self): """Génère les signatures placées en bas du bulletin PDF Renvoie une liste d'objets platypus """ show_left = self.preferences["bul_show_sig_left"] show_right = self.preferences["bul_show_sig_right"] if show_left or show_right: if show_left: L = [ [ sco_bulletins_pdf.process_field( self.preferences["bul_pdf_sig_left"], self.infos, self.FieldStyle, ) ] ] else: L = [[""]] if show_right: L[0].append( sco_bulletins_pdf.process_field( self.preferences["bul_pdf_sig_right"], self.infos, self.FieldStyle, ) ) else: L[0].append("") t = Table(L) t._argW[0] = 10 * cm # fixe largeur colonne gauche return [Spacer(1, 1.5 * cm), t] # espace vertical avant signatures else: return [] PDF_LINEWIDTH = 0.5 PDF_LINECOLOR = Color(0, 0, 0) PDF_MODSEPCOLOR = Color( 170 / 255.0, 170 / 255.0, 170 / 255.0 ) # lignes séparant les modules PDF_UE_CUR_BG = Color( 210 / 255.0, 210 / 255.0, 210 / 255.0 ) # fond UE courantes non prises en compte PDF_LIGHT_GRAY = Color(0.75, 0.75, 0.75) PDF_COLOR_CACHE = {} # (r,g,b) : pdf Color instance def ue_color(self, ue_type=UE_STANDARD): rgb_color = UE_COLORS.get(ue_type, UE_DEFAULT_COLOR) color = self.PDF_COLOR_CACHE.get(rgb_color, None) if not color: color = Color(*rgb_color) self.PDF_COLOR_CACHE[rgb_color] = color return color def ue_color_rgb(self, ue_type=UE_STANDARD): rgb_color = UE_COLORS.get(ue_type, UE_DEFAULT_COLOR) return "rgb(%d,%d,%d);" % ( rgb_color[0] * 255, rgb_color[1] * 255, rgb_color[2] * 255, ) def build_bulletin_table(self): """Génère la table centrale du bulletin de notes classique (pas BUT) 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) formsemestre_id = I["formsemestre_id"] prefs = sco_preferences.SemPreferences(formsemestre_id) # Colonnes à afficher: with_col_abs = prefs["bul_show_abs_modules"] with_col_minmax = ( prefs["bul_show_minmax"] or prefs["bul_show_minmax_mod"] or prefs["bul_show_minmax_eval"] ) with_col_moypromo = prefs["bul_show_moypromo"] with_col_rang = prefs["bul_show_rangs"] with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"] with_col_ects = prefs["bul_show_ects"] col_keys = ["titre", "module"] # noms des colonnes à afficher if with_col_rang: col_keys += ["rang"] if with_col_minmax: col_keys += ["min"] if with_col_moypromo: col_keys += ["moy"] if with_col_minmax: col_keys += ["max"] col_keys += ["note"] if with_col_coef: col_keys += ["coef"] if with_col_ects: col_keys += ["ects"] if with_col_abs: col_keys += ["abs"] colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus) i = 0 for k in col_keys: colidx[k] = i i += 1 if prefs["bul_pdf_mod_colwidth"]: bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm else: bul_pdf_mod_colwidth = None col_widths = { "titre": None, "module": bul_pdf_mod_colwidth, "min": 1.5 * cm, "moy": 1.5 * cm, "max": 1.5 * cm, "rang": 2.2 * cm, "note": 2 * cm, "coef": 1.5 * cm, "ects": 1.5 * cm, "abs": 2.0 * cm, } # HTML specific linktmpl = ( '<span onclick="toggle_vis_ue(this);" class="toggle_ue">%s</span> ' ) minuslink = linktmpl % scu.icontag("minus_img", border="0", alt="-") pluslink = linktmpl % scu.icontag("plus_img", border="0", alt="+") # 1er ligne titres t = { "min": "Promotion", "moy": "", "max": "", "rang": "Rang", "note": "Note/20", "coef": "Coef.", "ects": "ECTS", "abs": "Abs.", "_min_colspan": 2, "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [], } if with_col_moypromo and not with_col_minmax: t["moy"] = "Promotion" P.append(t) # 2eme ligne titres si nécessaire if with_col_minmax or with_col_abs: t = { "min": "mini", "moy": "moy.", "max": "maxi", "abs": "(Tot. / J.)", "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [], } P.append(t) P[-1]["_pdf_style"].append( ("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR) ) # Espacement sous la ligne moyenne générale: P[-1]["_pdf_style"].append(("BOTTOMPADDING", (0, 1), (-1, 1), 8)) # Moyenne générale: nbabs = I["nbabs"] nbabsjust = I["nbabsjust"] t = { "titre": "Moyenne générale:", "rang": I["rang_nt"], "note": I.get("moy_gen", "-"), "min": I.get("moy_min", "-"), "max": I.get("moy_max", "-"), "moy": I.get("moy_moy", "-"), "abs": "%s / %s" % (nbabs, nbabsjust), "_css_row_class": "notes_bulletin_row_gen", "_titre_colspan": 2, "_pdf_row_markup": ["b"], # bold. On peut ajouter 'font size="12"' "_pdf_style": [ ("LINEABOVE", (0, 1), (-1, 1), 1, self.PDF_LINECOLOR), ], } P.append(t) # Rangs dans les partitions: partitions, _ = sco_groups.get_formsemestre_groups(formsemestre_id) for partition in partitions: if partition["bul_show_rank"]: partition_id = partition["partition_id"] P.append( { "titre": "Rang dans %s %s: %s / %s inscrits" % ( partition["partition_name"], I["gr_name"][partition_id], I["rang_gr"][partition_id], I["ninscrits_gr"][partition_id], ), "_titre_colspan": 3, "_css_row_class": "notes_bulletin_row_rang", } ) # Chaque UE: for ue in I["ues"]: ue_type = None coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else "" ue_descr = ue["ue_descr_txt"] rowstyle = "" plusminus = minuslink # if ue["ue_status"]["is_capitalized"]: # UE capitalisée meilleure que UE courante: if prefs["bul_show_ue_cap_details"]: hidden = False cssstyle = "" plusminus = minuslink else: hidden = True cssstyle = "sco_hide" plusminus = pluslink try: ects_txt = str(int(ue["ects"])) except: ects_txt = "-" t = { "titre": ue["acronyme"] + " " + (ue["titre"] or ""), "_titre_html": plusminus + (ue["acronyme"] or "") + " " + (ue["titre"] or "") + ' <span class="bul_ue_descr">' + (ue["ue_descr_txt"] or "") + "</span>", "_titre_help": ue["ue_descr_txt"] or "", "_titre_colspan": 2, "module": ue_descr, "note": ue["moy_ue_txt"], "coef": coef_ue, "ects": ects_txt, "_css_row_class": "notes_bulletin_row_ue", "_tr_attrs": 'style="background-color: %s"' % self.ue_color_rgb(), "_pdf_row_markup": ["b"], "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), self.ue_color()), ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_LINECOLOR), ], } P.append(t) # Notes de l'UE capitalisée obtenues antérieurement: self._list_modules( ue["modules_capitalized"], matieres_modules=self.infos["matieres_modules_capitalized"], ue_type=ue_type, P=P, prefs=prefs, rowstyle=" bul_row_ue_cap %s" % cssstyle, hidden=hidden, ) ue_type = "cur" ue_descr = "" rowstyle = ( " bul_row_ue_cur" # style css pour indiquer UE non prise en compte ) try: ects_txt = str(int(ue["ects"])) except: ects_txt = "-" titre = f"{ue['acronyme'] or ''} {ue['titre'] or ''}" t = { "titre": titre, "_titre_html": minuslink + titre, "_titre_colspan": 2, "module": ue["titre"], "rang": ue_descr, "note": ue["cur_moy_ue_txt"], "coef": coef_ue, "ects": ects_txt, "_css_row_class": "notes_bulletin_row_ue", "_tr_attrs": 'style="background-color: %s"' % self.ue_color_rgb(ue_type=ue["type"]), "_pdf_row_markup": ["b"], "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), self.ue_color(ue_type=ue["type"])), ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_LINECOLOR), ], } if ue_type == "cur": t["module"] = "(en cours, non prise en compte)" t["_css_row_class"] += " notes_bulletin_row_ue_cur" t["_titre_help"] = "(en cours, non prise en compte)" if prefs["bul_show_minmax"]: t["min"] = scu.fmt_note(ue["min"]) t["max"] = scu.fmt_note(ue["max"]) if prefs["bul_show_moypromo"]: t["moy"] = scu.fmt_note(ue["moy"]).replace("NA", "-") # Cas particulier des UE sport (bonus) if ue["type"] == UE_SPORT and not ue_descr: del t["module"] del t["coef"] t["_pdf_style"].append(("SPAN", (colidx["note"], 0), (-1, 0))) # t["_module_colspan"] = 3 # non car bug si aucune colonne additionnelle # UE électives if ue["type"] == UE_ELECTIVE: t["module"] += " <i>(élective)</i>" if ue["modules"]: if ( ue_type != "cur" or prefs["bul_show_ue_cap_current"] ): # ne montre pas UE en cours et capitalisée, sauf si forcé P.append(t) self._list_modules( ue["modules"], matieres_modules=self.infos["matieres_modules"], ue_type=ue_type, P=P, prefs=prefs, rowstyle=rowstyle, ) # Ligne somme ECTS if with_col_ects: t = { "titre": "Crédits ECTS acquis:", "ects": str(int(I["sum_ects"])), # incluant les UE capitalisees "_css_row_class": "notes_bulletin_row_sum_ects", "_pdf_row_markup": ["b"], # bold "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), self.PDF_LIGHT_GRAY), ("LINEABOVE", (0, 0), (-1, 0), 1, 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 def _list_modules( self, ue_modules, matieres_modules={}, ue_type=None, P=None, prefs=None, rowstyle="", hidden=False, ): """Liste dans la table les descriptions des modules et, si version != short, des évaluations.""" if ue_type == "cur": # UE courante non prise en compte (car capitalisee) pdf_style_bg = [("BACKGROUND", (0, 0), (-1, 0), self.PDF_UE_CUR_BG)] else: pdf_style_bg = [] pdf_style = pdf_style_bg + [ ("LINEABOVE", (0, 0), (-1, 0), 1, self.PDF_MODSEPCOLOR), ("SPAN", (0, 0), (1, 0)), ] if ue_type == "cur": # UE courante non prise en compte (car capitalisee) pdf_style.append(("BACKGROUND", (0, 0), (-1, 0), self.PDF_UE_CUR_BG)) last_matiere_id = None for mod in ue_modules: if mod["mod_moy_txt"] == "NI": continue # saute les modules où on n'est pas inscrit # Matière: matiere_id = mod["module"]["matiere_id"] if prefs["bul_show_matieres"] and matiere_id != last_matiere_id: mat = matieres_modules[matiere_id] P.append( { "titre": mat["titre"], #'_titre_help' : matiere_id, "_titre_colspan": 2, "note": mat["moy_txt"], "_css_row_class": "notes_bulletin_row_mat%s" % rowstyle, "_pdf_style": pdf_style_bg + [("LINEABOVE", (0, 0), (-1, 0), 2, self.PDF_MODSEPCOLOR)], "_pdf_row_markup": ['font color="darkblue"'], } ) last_matiere_id = matiere_id # t = { "titre": mod["code_txt"] + " " + mod["name"], "_titre_colspan": 2, "rang": mod["mod_rang_txt"], # vide si pas option rang "note": mod["mod_moy_txt"], "coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "", "abs": mod.get( "mod_abs_txt", "" ), # absent si pas option show abs module "_css_row_class": "notes_bulletin_row_mod%s" % rowstyle, "_titre_target": "moduleimpl_status?moduleimpl_id=%s" % mod["moduleimpl_id"], "_titre_help": mod["mod_descr_txt"], "_hidden": hidden, "_pdf_style": pdf_style, } if prefs["bul_show_minmax_mod"]: t["min"] = scu.fmt_note(mod["stats"]["min"]) t["max"] = scu.fmt_note(mod["stats"]["max"]) if prefs["bul_show_moypromo"]: t["moy"] = scu.fmt_note(mod["stats"]["moy"]).replace("NA", "-") P.append(t) if self.version != "short": # --- notes de chaque eval: nbeval = self._list_evals( mod["evaluations"], P, rowstyle, pdf_style_bg, hidden, prefs=prefs ) # evals futures ou incomplètes: nbeval += self._list_evals( mod["evaluations_incompletes"], P, rowstyle, pdf_style_bg, hidden, incomplete=True, prefs=prefs, ) if nbeval: # boite autour des évaluations (en pdf) P[-1]["_pdf_style"].append( ("BOX", (1, 1 - nbeval), (-1, 0), 0.2, self.PDF_LIGHT_GRAY) ) def _list_evals( self, evals, P, rowstyle="", pdf_style_bg=[], hidden=False, incomplete=False, prefs={}, ): if incomplete: # style special pour evaluations incompletes: rowstyle += " notes_bulletin_row_eval_incomplete" pdf_row_markup = ['font color="red"'] else: pdf_row_markup = [] # --- notes de chaque eval: nbeval = 0 for e in evals: if e["visibulletin"] or self.version == "long": if nbeval == 0: eval_style = " b_eval_first" else: eval_style = "" t = { "module": '<bullet indent="2mm">•</bullet> ' + e["name"], "coef": ("<i>" + e["coef_txt"] + "</i>") if prefs["bul_show_coef"] else "", "_hidden": hidden, "_module_target": e["target_html"], # '_module_help' : , "_css_row_class": "notes_bulletin_row_eval" + eval_style + rowstyle, "_pdf_style": pdf_style_bg[:], "_pdf_row_markup": pdf_row_markup, } if e["note_txt"]: t["note"] = "<i>" + e["note_txt"] + "</i>" else: t["_module_colspan"] = 2 if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]: etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) if prefs["bul_show_minmax_eval"]: t["min"] = scu.fmt_note(etat["mini"]) t["max"] = scu.fmt_note(etat["maxi"]) if prefs["bul_show_moypromo"]: t["moy"] = scu.fmt_note(etat["moy"]) P.append(t) nbeval += 1 return nbeval # sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)