############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Génération bulletin BUT au format PDF standard La génération du bulletin PDF suit le chemin suivant: - vue formsemestre_bulletinetud -> sco_bulletins.formsemestre_bulletinetud bul_dict = bulletin_but.BulletinBUT(formsemestre).bulletin_etud_complet(etud) - sco_bulletins_generator.make_formsemestre_bulletin_etud() - instance de BulletinGeneratorStandardBUT - BulletinGeneratorStandardBUT.generate(fmt="pdf") sco_bulletins_generator.BulletinGenerator.generate() .generate_pdf() .bul_table() (ci-dessous) """ from reportlab.lib.colors import blue from reportlab.lib.units import cm, mm from reportlab.platypus import Paragraph, Spacer from app.models import Evaluation, ScoDocSiteConfig from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc import gen_tables from app.scodoc.codes_cursus import UE_SPORT from app.scodoc import sco_utils as scu class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): """Génération du bulletin de BUT au format PDF. self.infos est le dict issu de BulletinBUT.bulletin_etud_complet() """ # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur: list_in_menu = False scale_table_in_page = False # pas de mise à l'échelle pleine page auto multi_pages = True # plusieurs pages par bulletins small_fontsize = "8" def bul_table(self, fmt="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). """ if fmt == "pdf" and ScoDocSiteConfig.is_bul_pdf_disabled(): return [Paragraph("

Export des PDF interdit par l'administrateur

")] tables_infos = [ # ---- TABLE SYNTHESE UES self.but_table_synthese_ues(), ] if self.version != "short": tables_infos += [ # ---- TABLE RESSOURCES self.but_table_ressources(), # ---- TABLE SAE self.but_table_saes(), ] objects = [] for i, (col_keys, rows, pdf_style, col_widths) in enumerate(tables_infos): table = gen_tables.GenTable( rows=rows, columns_ids=col_keys, pdf_table_style=pdf_style, pdf_col_widths=[col_widths[k] for k in col_keys], preferences=self.preferences, html_class="notes_bulletin", html_class_ignore_default=True, html_with_td_classes=True, ) table_objects = table.gen(fmt=fmt) objects += table_objects # objects += [KeepInFrame(0, 0, table_objects, mode="shrink")] if i != 2: objects.append(Spacer(1, 6 * mm)) return objects def but_table_synthese_ues( self, title_bg=(182, 235, 255), title_ue_cap_bg=(150, 207, 147) ): """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 """ # nb: self.infos a ici été donné par BulletinBUT.bulletin_etud_complet() col_widths = { "titre": None, "min": 1.5 * cm, "moy": 1.5 * cm, "max": 1.5 * cm, "moyenne": 2 * cm, "coef": 2 * cm, } with_col_minmax = self.preferences["bul_show_minmax"] with_col_moypromo = self.preferences["bul_show_moypromo"] # Noms des colonnes à afficher: col_keys = ["titre"] if with_col_minmax: col_keys += ["min"] if with_col_moypromo: col_keys += ["moy"] if with_col_minmax: col_keys += ["max"] col_keys += ["coef", "moyenne"] # Couleur fond: title_bg = tuple(x / 255.0 for x in title_bg) title_ue_cap_bg = tuple(x / 255.0 for x in title_ue_cap_bg) # elems pour générer table avec gen_table (liste de dicts) rows = [ # Ligne de titres { "titre": "Unités d'enseignement", "min": "Promotion", "moy": "", "max": "", "_min_colspan": 2, "moyenne": Paragraph("Note/20"), "coef": "Coef.", "_coef_pdf": Paragraph("Coef."), "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), title_bg), ], }, ] if with_col_moypromo and not with_col_minmax: rows[-1]["moy"] = "Promotion" # 2eme ligne titres si nécessaire if with_col_minmax: # TODO or with_col_abs: rows.append( { "min": "min.", "moy": "moy.", "max": "max.", # "abs": "(Tot. / J.)", "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), title_bg), ], } ) rows[-1]["_pdf_style"] += [ ("BOTTOMPADDING", (0, 0), (-1, 0), 7), ( "LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, blue, ), ] ues = self.infos["ues"] ues_capitalisees = self.infos.get("ues_capitalisees", {}) ues_tup = sorted( list(ues.items()) + list(ues_capitalisees.items()), key=lambda x: x[1]["numero"], ) for ue_acronym, ue in ues_tup: is_capitalized = "date_capitalisation" in ue self._ue_rows( rows, ue_acronym, ue, title_ue_cap_bg if is_capitalized else title_bg ) # 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, rows, pdf_style, col_widths def _ue_rows(self, rows: list, ue_acronym: str, ue: dict, title_bg: tuple): "Décrit une UE dans la table synthèse: titre, sous-titre et liste modules" if (ue["type"] == UE_SPORT) and len(ue.get("modules", [])) == 0: # ne mentionne l'UE que s'il y a des modules return # 1er ligne titre UE moy_ue = ue.get("moyenne", "-") if isinstance(moy_ue, dict): moy_ue = moy_ue.get("value", "-") if moy_ue is not None else "-" t = { "titre": f"{ue_acronym} - {ue['titre']}", "moyenne": Paragraph( f"""{moy_ue or "-"}""" ), "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [ ( "LINEABOVE", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR, ), ("BACKGROUND", (0, 0), (-1, 0), title_bg), ("BOTTOMPADDING", (0, 0), (-1, 0), 7), ], } rows.append(t) if ue["type"] == UE_SPORT: self.ue_sport_rows(rows, ue, title_bg) else: self.ue_std_rows(rows, ue, title_bg) @staticmethod def affichage_bonus_malus(ue: dict) -> list[str]: "liste de chaînes affichant les bonus et malus" fields_bmr = [] # lecture des bonus sport culture et malus (ou bonus autre) (0 si valeur non numérique) try: bonus_sc = float(ue.get("bonus", 0.0)) or 0 except ValueError: bonus_sc = 0 try: malus = float(ue.get("malus", 0.0)) or 0 except ValueError: malus = 0 # Calcul de l affichage if malus < 0: if bonus_sc > 0: fields_bmr.append(f"Bonus sport/culture: {bonus_sc}") fields_bmr.append(f"Bonus autres: {-malus}") else: fields_bmr.append(f"Bonus: {-malus}") elif malus > 0: if bonus_sc > 0: fields_bmr.append(f"Bonus: {bonus_sc}") fields_bmr.append(f"Malus: {malus}") else: if bonus_sc > 0: fields_bmr.append(f"Bonus: {bonus_sc}") return fields_bmr def ue_std_rows(self, rows: list, ue: dict, title_bg: tuple): "Lignes décrivant une UE standard dans la table de synthèse" # 2eme ligne titre UE (bonus/malus/ects) if "ECTS" in ue: ects_txt = f'ECTS: {ue["ECTS"]["acquis"]:.3g} / {ue["ECTS"]["total"]:.3g}' else: ects_txt = "" # case Bonus/Malus/Rang "bmr" fields_bmr = BulletinGeneratorStandardBUT.affichage_bonus_malus(ue) moy_ue = ue.get("moyenne", "-") if isinstance(moy_ue, dict): # UE non capitalisées if self.preferences["bul_show_ue_rangs"]: fields_bmr.append( f"Rang: {ue['moyenne']['rang']} / {ue['moyenne']['total']}" ) ue_min, ue_max, ue_moy = ( ue["moyenne"]["min"], ue["moyenne"]["max"], ue["moyenne"]["moy"], ) else: # UE capitalisée ue_min, ue_max, ue_moy = "", "", moy_ue date_capitalisation = ue.get("date_capitalisation") if date_capitalisation: fields_bmr.append( f"""Capitalisée le {date_capitalisation.strftime(scu.DATE_FMT)}""" ) t = { "titre": " - ".join(fields_bmr), "coef": ects_txt, "_coef_pdf": Paragraph(f"""{ects_txt}"""), "_coef_colspan": 2, "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), title_bg), ("LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR), # ligne au dessus du bonus/malus, gris clair ("LINEABOVE", (0, 0), (-1, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7)), ], "min": ue_min, "max": ue_max, "moy": ue_moy, } rows.append(t) # Liste chaque ressource puis chaque SAE for mod_type in ("ressources", "saes"): for mod_code, mod in ue[mod_type].items(): t = { "titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}", "moyenne": Paragraph(f'{mod["moyenne"]}'), "coef": mod["coef"], "_coef_pdf": Paragraph( f"{mod['coef']}" ), "_pdf_style": [ ( "LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7), # gris clair ) ], } rows.append(t) def ue_sport_rows(self, rows: list, ue: dict, title_bg: tuple): "Lignes décrivant l'UE bonus dans la table de synthèse" # UE BONUS for mod_code, mod in ue["modules"].items(): rows.append( { "titre": f"{mod_code or ''} {mod['titre'] or ''}", } ) self.evaluations_rows(rows, mod["evaluations"]) def but_table_ressources(self): """La table de synthèse; pour chaque ressources, note et liste d'évaluations Renvoie: colkeys, P, pdf_style, colWidths """ return self.bul_table_modules( mod_type="ressources", title="Ressources", title_bg=(248, 200, 68) ) def but_table_saes(self): "table des SAEs" return self.bul_table_modules( mod_type="saes", title="Situations d'apprentissage et d'évaluation", title_bg=(198, 255, 171), ) def bul_table_modules(self, mod_type=None, title="", title_bg=(248, 200, 68)): """Table ressources ou SAEs - 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 """ # UE à utiliser pour les poids (# colonne/UE) ue_infos = self.infos["ues"] ue_acros = list( [k for k in ue_infos if ue_infos[k]["type"] != UE_SPORT] ) # ['RT1.1', 'RT2.1', 'RT3.1'] # Colonnes à afficher: col_keys = ["titre"] + ue_acros + ["coef", "moyenne"] # Largeurs des colonnes: col_widths = { "titre": None, # "poids": None, "moyenne": 2 * cm, "coef": 2 * cm, } for ue_acro in ue_acros: col_widths[ue_acro] = 12 * mm # largeur col. poids title_bg = tuple(x / 255.0 for x in title_bg) # elems pour générer table avec gen_table (liste de dicts) # Ligne de titres t = { "titre": title, # "_titre_colspan": 1 + len(ue_acros), "moyenne": "Note/20", "coef": "Coef.", "_coef_pdf": Paragraph("Coef."), "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [ ("BACKGROUND", (0, 0), (-1, 0), title_bg), ("BOTTOMPADDING", (0, 0), (-1, 0), 7), ( "LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, blue, ), ], } for ue_acro in ue_acros: t[ue_acro] = Paragraph( f"{ue_acro}" ) rows = [t] for mod_code, mod in self.infos[mod_type].items(): # 1er ligne titre module t = { "titre": f"{mod_code} - {mod['titre']}", "_titre_colspan": 2 + len(ue_acros), "_css_row_class": "note_bold", "_pdf_row_markup": ["b"], "_pdf_style": [ ( "LINEABOVE", (0, 0), (-1, 0), self.PDF_LINEWIDTH, self.PDF_LINECOLOR, ), ("BACKGROUND", (0, 0), (-1, 0), title_bg), ("BOTTOMPADDING", (0, 0), (-1, 0), 7), ], } rows.append(t) # Evaluations: self.evaluations_rows(rows, mod["evaluations"], ue_acros) # 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, rows, pdf_style, col_widths def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()): "lignes des évaluations" for e in evaluations: coef = ( e["coef"] if e["evaluation_type"] == Evaluation.EVALUATION_NORMALE else "*" ) note_value = e["note"].get("value", "") t = { "titre": f"{e['description'] or ''}", "moyenne": note_value, "_moyenne_pdf": Paragraph(f"""{note_value}"""), "coef": coef, "_coef_pdf": Paragraph( f"""{ coef if e["evaluation_type"] != Evaluation.EVALUATION_BONUS else "bonus" }""" ), "_pdf_style": [ ( "LINEBELOW", (0, 0), (-1, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7), # gris clair ) ], } col_idx = 1 # 1ere col. poids for ue_acro in ue_acros: t[ue_acro] = Paragraph( f"""{ e["poids"].get(ue_acro, "") or ""}""" ) t["_pdf_style"].append( ( "BOX", (col_idx, 0), (col_idx, 0), self.PDF_LINEWIDTH, (0.7, 0.7, 0.7), # gris clair ), ) col_idx += 1 rows.append(t)