diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index f57e2254..bc6dca94 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -228,7 +228,7 @@ class BonusSportAdditif(BonusSport): axis=1, ) # Seuil: bonus dans [min, max] (défaut [0,20]) - bonus_max = self.bonus_max or 0.0 + bonus_max = self.bonus_max or 20.0 np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr) self.bonus_additif(bonus_moy_arr) @@ -418,17 +418,46 @@ class BonusAmiens(BonusSportAdditif): class BonusBethune(BonusSportMultiplicatif): - """Calcul bonus modules optionnels (sport), règle IUT de Béthune. - - Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre. - Ce bonus est égal au nombre de points divisé par 200 et multiplié par la - moyenne générale du semestre de l'étudiant. + """ + Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune. +

+ Pour le BUT : + La note de sport est sur 20, et on calcule une bonification (en %) + qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant + la formule : bonification (en %) = max(note-10, 0)*(1/500). +

+ La bonification ne s'applique que si la note est supérieure à 10. +

+ (Une note de 10 donne donc 0% de bonif, + 1 point au dessus de 10 augmente la moyenne des UE de 0.2%) +

+

+ Pour le DUT/LP : + La note de sport est sur 20, et on calcule une bonification (en %) + qui va s'appliquer à la moyenne générale du semestre en appliquant + la formule : bonification (en %) = max(note-10, 0)*(1/200). +

+ La bonification ne s'applique que si la note est supérieure à 10. +

+ (Une note de 10 donne donc 0% de bonif, + 1 point au dessus de 10 augmente la moyenne des UE de 0.5%) +

""" name = "bonus_iutbethune" displayed_name = "IUT de Béthune" - seuil_moy_gen = 10.0 - amplitude = 0.005 + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + if self.formsemestre.formation.is_apc(): + self.amplitude = 0.002 + else: + self.amplitude = 0.005 + + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) class BonusBezier(BonusSportAdditif): @@ -502,10 +531,11 @@ class BonusCachan1(BonusSportAdditif): @@ -516,6 +546,7 @@ class BonusCachan1(BonusSportAdditif): seuil_moy_gen = 10.0 # tous les points sont comptés proportion_point = 0.03 classic_use_bonus_ues = True + ues_bonifiables_cachan = {"UE13_E", "UE23_E", "UE33_E", "UE43_E"} def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): """calcul du bonus, avec réglage différent suivant le type de formation""" @@ -540,7 +571,7 @@ class BonusCachan1(BonusSportAdditif): dtype=float, ) else: # --- DUT - # pareil mais proportion différente et exclusion d'une UE + # pareil mais proportion différente et application à certaines UEs proportion_point = 0.1 bonus_moy_arr = np.where( note_bonus_max > self.seuil_moy_gen, @@ -553,10 +584,10 @@ class BonusCachan1(BonusSportAdditif): columns=ues_idx, dtype=float, ) - # Pas de bonus sur la ou les ue de code "UE41_E" - ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"] - for ue in ue_exclues: - self.bonus_ues[ue.id] = 0.0 + # Applique bonus seulement sur certaines UE de code connu: + for ue in ues: + if ue.ue_code not in self.ues_bonifiables_cachan: + self.bonus_ues[ue.id] = 0.0 # annule class BonusCalais(BonusSportAdditif): @@ -982,7 +1013,7 @@ class BonusTarbes(BonusSportAdditif): """ name = "bonus_tarbes" - displayed_name = "IUT de Tazrbes" + displayed_name = "IUT de Tarbes" seuil_moy_gen = 10.0 proportion_point = 1 / 30.0 classic_use_bonus_ues = True diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index eea357e8..13829a39 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -92,6 +92,8 @@ class ModuleImplResults: ou NaN si les évaluations (dans lesquelles l'étudiant a des notes) ne donnent pas de coef vers cette UE. """ + self.evals_etudids_sans_note = {} + """dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions.""" self.load_notes() self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index) """1 bool par etud, indique si sa moyenne de module vient de la session2""" @@ -142,12 +144,13 @@ class ModuleImplResults: # ou évaluation déclarée "à prise en compte immédiate" # Les évaluations de rattrapage et 2eme session sont toujours incomplètes # car on calcule leur moyenne à part. + etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and ( - evaluation.publish_incomplete - or (not (inscrits_module - set(eval_df.index))) + evaluation.publish_incomplete or (not etudids_sans_note) ) self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete + self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note # NULL en base => ABS (= -999) eval_df.fillna(scu.NOTES_ABSENCE, inplace=True) @@ -193,7 +196,9 @@ class ModuleImplResults: return eval_df def _etudids(self): - """L'index du dataframe est la liste de tous les étudiants inscrits au semestre""" + """L'index du dataframe est la liste de tous les étudiants inscrits au semestre + (incluant les DEM et DEF) + """ return [ inscr.etudid for inscr in ModuleImpl.query.get( diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 1f19fbfd..7bc199ea 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -14,18 +14,19 @@ import pandas as pd from flask import g, url_for +from app.auth.models import User from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, FormSemestreUECoef +from app.models import FormSemestre, FormSemestreUECoef, formsemestre from app.models import Identite from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM +from app.scodoc import sco_evaluation_db from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_groups -from app.scodoc import sco_users from app.scodoc import sco_utils as scu # Il faut bien distinguer @@ -387,7 +388,9 @@ class ResultatsSemestre(ResultatsCache): # --- TABLEAU RECAP - def get_table_recap(self, convert_values=False): + def get_table_recap( + self, convert_values=False, include_evaluations=False, modejury=False + ): """Result: tuple avec - rows: liste de dicts { column_id : value } - titles: { column_id : title } @@ -413,7 +416,6 @@ class ResultatsSemestre(ResultatsCache): - les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_ __order : clé de tri """ - if convert_values: fmt_note = scu.fmt_note else: @@ -429,6 +431,7 @@ class ResultatsSemestre(ResultatsCache): titles = {} # les titres en footer: les mêmes, mais avec des bulles et liens: titles_bot = {} + dict_nom_res = {} # cache uid : nomcomplet def add_cell( row: dict, @@ -457,6 +460,11 @@ class ResultatsSemestre(ResultatsCache): idx = 0 # index de la colonne etud = Identite.query.get(etudid) row = {"etudid": etudid} + # --- Codes (seront cachés, mais exportés en excel) + idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx) + idx = add_cell( + row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx + ) # --- Rang idx = add_cell( row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx @@ -532,22 +540,28 @@ class ResultatsSemestre(ResultatsCache): titles_bot[ f"_{col_id}_target_attrs" ] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """ + if modejury: + # pas d'autre colonnes de résultats + continue # Bonus (sport) dans cette UE ? # Le bonus sport appliqué sur cette UE if (self.bonus_ues is not None) and (ue.id in self.bonus_ues): val = self.bonus_ues[ue.id][etud.id] or "" - val_fmt = fmt_note(val) + val_fmt = val_fmt_html = fmt_note(val) if val: - val_fmt = f'{val_fmt}' + val_fmt_html = f'{val_fmt}' idx = add_cell( row, f"bonus_ue_{ue.id}", f"Bonus {ue.acronyme}", - val_fmt, + val_fmt_html, "col_ue_bonus", idx, ) + row[f"_bonus_ue_{ue.id}_xls"] = val_fmt # Les moyennes des modules (ou ressources et SAÉs) dans cette UE + idx_malus = idx # place pour colonne malus à gauche des modules + idx += 1 for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False): if ue_status["is_capitalized"]: val = "-c-" @@ -573,63 +587,87 @@ class ResultatsSemestre(ResultatsCache): col_id = ( f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" ) - val_fmt = fmt_note(val) + val_fmt = val_fmt_html = fmt_note(val) if modimpl.module.module_type == scu.ModuleType.MALUS: - val_fmt = ( + val_fmt_html = ( (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" ) idx = add_cell( row, col_id, modimpl.module.code, - val_fmt, + val_fmt_html, # class col_res mod_ue_123 f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}", idx, ) + row[f"_{col_id}_xls"] = val_fmt + if modimpl.module.module_type == scu.ModuleType.MALUS: + titles[f"_{col_id}_col_order"] = idx_malus titles_bot[f"_{col_id}_target"] = url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, ) + nom_resp = dict_nom_res.get(modimpl.responsable_id) + if nom_resp is None: + user = User.query.get(modimpl.responsable_id) + nom_resp = user.get_nomcomplet() if user else "" + dict_nom_res[modimpl.responsable_id] = nom_resp titles_bot[ f"_{col_id}_target_attrs" - ] = f""" - title="{modimpl.module.titre} - ({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """ + ] = f""" title="{modimpl.module.titre} ({nom_resp})" """ modimpl_ids.add(modimpl.id) - ue_valid_txt = f"{nb_ues_validables}/{len(ues_sans_bonus)}" + ue_valid_txt = ( + ue_valid_txt_html + ) = f"{nb_ues_validables}/{len(ues_sans_bonus)}" if nb_ues_warning: - ue_valid_txt += " " + scu.EMO_WARNING + ue_valid_txt_html += " " + scu.EMO_WARNING add_cell( row, "ues_validables", "UEs", - ue_valid_txt, + ue_valid_txt_html, "col_ue col_ues_validables", 29, # juste avant moy. gen. ) + row["_ues_validables_xls"] = ue_valid_txt if nb_ues_warning: row["_ues_validables_class"] += " moy_ue_warning" elif nb_ues_validables < len(ues_sans_bonus): row["_ues_validables_class"] += " moy_inf" row["_ues_validables_order"] = nb_ues_validables # pour tri + if modejury: + idx = add_cell( + row, + "jury_link", + "", + f"""saisir décision""", + "col_jury_link", + 1000, + ) rows.append(row) self._recap_add_partitions(rows, titles) self._recap_add_admissions(rows, titles) + # tri par rang croissant rows.sort(key=lambda e: e["_rang_order"]) # INFOS POUR FOOTER bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note) + if include_evaluations: + self._recap_add_evaluations(rows, titles, bottom_infos) # Ajoute style "col_empty" aux colonnes de modules vides for col_id in titles: c_class = f"_{col_id}_class" if "col_empty" in bottom_infos["moy"].get(c_class, ""): for row in rows: - row[c_class] += " col_empty" + row[c_class] = row.get(c_class, "") + " col_empty" titles[c_class] += " col_empty" for row in bottom_infos.values(): row[c_class] = row.get(c_class, "") + " col_empty" @@ -641,7 +679,9 @@ class ResultatsSemestre(ResultatsCache): row["moy_gen"] = row.get("moy_gen", "") row["_moy_gen_class"] = "col_moy_gen" # titre de la ligne: - row["prenom"] = row["nom_short"] = bottom_line.capitalize() + row["prenom"] = row["nom_short"] = ( + row.get("_title", "") or bottom_line.capitalize() + ) row["_tr_class"] = bottom_line.lower() + ( (" " + row["_tr_class"]) if "_tr_class" in row else "" ) @@ -656,53 +696,58 @@ class ResultatsSemestre(ResultatsCache): def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict: """Les informations à mettre en bas de la table: min, max, moy, ECTS""" - row_min, row_max, row_moy, row_coef, row_ects = ( - {"_tr_class": "bottom_info"}, + row_min, row_max, row_moy, row_coef, row_ects, row_apo = ( + {"_tr_class": "bottom_info", "_title": "Min."}, {"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"}, {"_tr_class": "bottom_info"}, + {"_tr_class": "bottom_info", "_title": "Code Apogée"}, ) # --- ECTS for ue in ues: - row_ects[f"moy_ue_{ue.id}"] = ue.ects - row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue" + colid = f"moy_ue_{ue.id}" + row_ects[colid] = ue.ects + row_ects[f"_{colid}_class"] = "col_ue" # style cases vides pour borders verticales - row_coef[f"moy_ue_{ue.id}"] = "" - row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue" + row_coef[colid] = "" + row_coef[f"_{colid}_class"] = "col_ue" + # row_apo[colid] = ue.code_apogee or "" row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]) row_ects["_moy_gen_class"] = "col_moy_gen" - # --- MIN, MAX, MOY + # --- MIN, MAX, MOY, APO row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min()) row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max()) row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean()) for ue in ues: - col_id = f"moy_ue_{ue.id}" - row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min()) - row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max()) - row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean()) - row_min[f"_{col_id}_class"] = "col_ue" - row_max[f"_{col_id}_class"] = "col_ue" - row_moy[f"_{col_id}_class"] = "col_ue" + colid = f"moy_ue_{ue.id}" + row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min()) + row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max()) + row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean()) + row_min[f"_{colid}_class"] = "col_ue" + row_max[f"_{colid}_class"] = "col_ue" + row_moy[f"_{colid}_class"] = "col_ue" + row_apo[colid] = ue.code_apogee or "" for modimpl in self.formsemestre.modimpls_sorted: if modimpl.id in modimpl_ids: - col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" + colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" if self.is_apc: coef = self.modimpl_coefs_df[modimpl.id][ue.id] else: coef = modimpl.module.coefficient or 0 - row_coef[col_id] = fmt_note(coef) + row_coef[colid] = fmt_note(coef) notes = self.modimpl_notes(modimpl.id, ue.id) - row_min[col_id] = fmt_note(np.nanmin(notes)) - row_max[col_id] = fmt_note(np.nanmax(notes)) + row_min[colid] = fmt_note(np.nanmin(notes)) + row_max[colid] = fmt_note(np.nanmax(notes)) moy = np.nanmean(notes) - row_moy[col_id] = fmt_note(moy) + row_moy[colid] = fmt_note(moy) if np.isnan(moy): # aucune note dans ce module - row_moy[f"_{col_id}_class"] = "col_empty" + row_moy[f"_{colid}_class"] = "col_empty" + row_apo[colid] = modimpl.module.code_apogee or "" return { # { key : row } avec key = min, max, moy, coef "min": row_min, @@ -710,6 +755,7 @@ class ResultatsSemestre(ResultatsCache): "moy": row_moy, "coef": row_coef, "ects": row_ects, + "apo": row_apo, } def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict): @@ -803,3 +849,68 @@ class ResultatsSemestre(ResultatsCache): row[f"{cid}"] = gr_name row[f"_{cid}_class"] = klass first_partition = False + + def _recap_add_evaluations( + self, rows: list[dict], titles: dict, bottom_infos: dict + ): + """Ajoute les colonnes avec les notes aux évaluations + rows est une liste de dict avec une clé "etudid" + Les colonnes ont la classe css "evaluation" + """ + # nouvelle ligne pour description évaluations: + bottom_infos["descr_evaluation"] = { + "_tr_class": "bottom_info", + "_title": "Description évaluation", + } + first_eval = True + index_col = 9000 # à droite + for modimpl in self.formsemestre.modimpls_sorted: + evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl) + eval_index = len(evals) - 1 + inscrits = {i.etudid for i in modimpl.inscriptions} + first_eval_of_mod = True + for e in evals: + cid = f"eval_{e.id}" + titles[ + cid + ] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}' + klass = "evaluation" + if first_eval: + klass += " first" + elif first_eval_of_mod: + klass += " first_of_mod" + titles[f"_{cid}_class"] = klass + first_eval_of_mod = first_eval = False + titles[f"_{cid}_col_order"] = index_col + index_col += 1 + eval_index -= 1 + notes_db = sco_evaluation_db.do_evaluation_get_all_notes( + e.evaluation_id + ) + for row in rows: + etudid = row["etudid"] + if etudid in inscrits: + if etudid in notes_db: + val = notes_db[etudid]["value"] + else: + # Note manquante mais prise en compte immédiate: affiche ATT + val = scu.NOTES_ATTENTE + row[cid] = scu.fmt_note(val) + row[f"_{cid}_class"] = klass + { + "ABS": " abs", + "ATT": " att", + "EXC": " exc", + }.get(row[cid], "") + else: + row[cid] = "ni" + row[f"_{cid}_class"] = klass + " non_inscrit" + + bottom_infos["coef"][cid] = e.coefficient + bottom_infos["min"][cid] = "0" + bottom_infos["max"][cid] = scu.fmt_note(e.note_max) + bottom_infos["descr_evaluation"][cid] = e.description or "" + bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for( + "notes.evaluation_listenotes", + scodoc_dept=g.scodoc_dept, + evaluation_id=e.id, + ) diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index facbf85f..9a5e1198 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -70,5 +70,16 @@ class CodesDecisionsForm(FlaskForm): DEM = _build_code_field("DEM") NAR = _build_code_field("NAR") RAT = _build_code_field("RAT") + NOTES_FMT = StringField( + label="Format notes exportées", + description="""Format des notes. Par défaut %3.2f (deux chiffres après la virgule)""", + validators=[ + validators.Length( + max=SHORT_STR_LEN, + message=f"Le format ne doit pas dépasser {SHORT_STR_LEN} caractères", + ), + validators.DataRequired("format requis"), + ], + ) submit = SubmitField("Valider") cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/config.py b/app/models/config.py index 1271beeb..53ac96e9 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -36,6 +36,7 @@ CODES_SCODOC_TO_APO = { DEM: "NAR", NAR: "NAR", RAT: "ATT", + "NOTES_FMT": "%3.2f", } @@ -157,32 +158,6 @@ class ScoDocSiteConfig(db.Model): class_list.sort(key=lambda x: x[1].replace(" du ", " de ")) return [("", "")] + class_list - @classmethod - def get_bonus_sport_func(cls): - """Fonction bonus_sport ScoDoc 7 XXX - Transitoire pour les tests durant la transition #sco92 - """ - """returns bonus func with specified name. - If name not specified, return the configured function. - None if no bonus function configured. - Raises ScoValueError if func_name not found in module bonus_sport. - """ - from app.scodoc import bonus_sport - - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c is None: - return None - func_name = c.value - if func_name == "": # pas de bonus défini - return None - try: - return getattr(bonus_sport, func_name) - except AttributeError: - raise ScoValueError( - f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}". - (contacter votre administrateur local).""" - ) - @classmethod def get_code_apo(cls, code: str) -> str: """La représentation d'un code pour les exports Apogée. diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 6c342482..962e7baa 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -16,6 +16,7 @@ from app import models from app.scodoc import notesdb as ndb from app.scodoc.sco_bac import Baccalaureat +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu @@ -354,7 +355,10 @@ def make_etud_args( """ args = None if etudid: - args = {"etudid": etudid} + try: + args = {"etudid": int(etudid)} + except ValueError as exc: + raise ScoValueError("Adresse invalide") from exc elif code_nip: args = {"code_nip": code_nip} elif use_request: # use form from current request (Flask global) diff --git a/app/models/groups.py b/app/models/groups.py index f6452cf7..9cf5f236 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -74,6 +74,10 @@ class GroupDescr(db.Model): f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">""" ) + def get_nom_with_part(self) -> str: + "Nom avec partition: 'TD A'" + return f"{self.partition.partition_name or ''} {self.group_name or '-'}" + group_membership = db.Table( "group_membership", diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 292ec8ff..7574ed7e 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -9,7 +9,6 @@ from app.comp import df_cache 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 7f5531c6..2136ee84 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -209,7 +209,8 @@ class GenTable(object): omit_hidden_lines=False, pdf_mode=False, # apply special pdf reportlab processing pdf_style_list=[], # modified: list of platypus table style commands - ): + xls_mode=False, # get xls content if available + ) -> list: "table data as a list of lists (rows)" T = [] line_num = 0 # line number in input data @@ -237,9 +238,14 @@ class GenTable(object): # if colspan_count > 0: # continue # skip cells after a span if pdf_mode: - content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or "" + content = row.get(f"_{cid}_pdf", False) or row.get(cid, "") + elif xls_mode: + content = row.get(f"_{cid}_xls", False) or row.get(cid, "") else: - content = row.get(cid, "") or "" # nota: None converted to '' + content = row.get(cid, "") + # Convert None to empty string "" + content = "" if content is None else content + colspan = row.get("_%s_colspan" % cid, 0) if colspan > 1: pdf_style_list.append( @@ -299,7 +305,7 @@ class GenTable(object): return self.xml() elif format == "json": return self.json() - raise ValueError("GenTable: invalid format: %s" % format) + raise ValueError(f"GenTable: invalid format: {format}") def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""): "row is a dict, returns a string ..." @@ -479,23 +485,23 @@ class GenTable(object): def excel(self, wb=None): """Simple Excel representation of the table""" if wb is None: - ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) + sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) else: - ses = wb.create_sheet(sheet_name=self.xls_sheet_name) - ses.rows += self.xls_before_table + sheet = wb.create_sheet(sheet_name=self.xls_sheet_name) + sheet.rows += self.xls_before_table style_bold = sco_excel.excel_make_style(bold=True) style_base = sco_excel.excel_make_style() - ses.append_row(ses.make_row(self.get_titles_list(), style_bold)) - for line in self.get_data_list(): - ses.append_row(ses.make_row(line, style_base)) + sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold)) + for line in self.get_data_list(xls_mode=True): + sheet.append_row(sheet.make_row(line, style_base)) if self.caption: - ses.append_blank_row() # empty line - ses.append_single_cell_row(self.caption, style_base) + sheet.append_blank_row() # empty line + sheet.append_single_cell_row(self.caption, style_base) if self.origin: - ses.append_blank_row() # empty line - ses.append_single_cell_row(self.origin, style_base) + sheet.append_blank_row() # empty line + sheet.append_single_cell_row(self.origin, style_base) if wb is None: - return ses.generate() + return sheet.generate() def text(self): "raw text representation of the table" diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index 53efbfc9..c700c1b8 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -86,9 +86,9 @@ def sidebar(): f"""