diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 6e37a8c2..d7240598 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -542,17 +542,18 @@ class ResultatsSemestre(ResultatsCache): # 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 @@ -581,20 +582,21 @@ 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( @@ -611,17 +613,20 @@ class ResultatsSemestre(ResultatsCache): f"_{col_id}_target_attrs" ] = 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): 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/sco_archives.py b/app/scodoc/sco_archives.py index 0103caa2..773fc042 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -47,14 +47,15 @@ qui est une description (humaine, format libre) de l'archive. """ -import chardet import datetime import glob +import json import mimetypes import os import re import shutil import time +import chardet import flask from flask import g, request @@ -63,7 +64,9 @@ from flask_login import current_user import app.scodoc.sco_utils as scu from config import Config from app import log -from app.models import Departement +from app.comp import res_sem +from app.comp.res_compat import NotesTableCompat +from app.models import Departement, FormSemestre from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_exceptions import ( AccessDenied, @@ -95,8 +98,8 @@ class BaseArchiver(object): self.root = os.path.join(*dirs) log("initialized archiver, path=" + self.root) path = dirs[0] - for dir in dirs[1:]: - path = os.path.join(path, dir) + for directory in dirs[1:]: + path = os.path.join(path, directory) try: scu.GSL.acquire() if not os.path.isdir(path): @@ -117,11 +120,11 @@ class BaseArchiver(object): try: scu.GSL.acquire() if not os.path.isdir(dept_dir): - log("creating directory %s" % dept_dir) + log(f"creating directory {dept_dir}") os.mkdir(dept_dir) obj_dir = os.path.join(dept_dir, str(oid)) if not os.path.isdir(obj_dir): - log("creating directory %s" % obj_dir) + log(f"creating directory {obj_dir}") os.mkdir(obj_dir) finally: scu.GSL.release() @@ -163,8 +166,9 @@ class BaseArchiver(object): def get_archive_date(self, archive_id): """Returns date (as a DateTime object) of an archive""" - dt = [int(x) for x in os.path.split(archive_id)[1].split("-")] - return datetime.datetime(*dt) + return datetime.datetime( + *[int(x) for x in os.path.split(archive_id)[1].split("-")] + ) def list_archive(self, archive_id: str) -> str: """Return list of filenames (without path) in archive""" @@ -195,8 +199,7 @@ class BaseArchiver(object): archive_id = os.path.join(self.get_obj_dir(oid), archive_name) if not os.path.isdir(archive_id): log( - "invalid archive name: %s, oid=%s, archive_id=%s" - % (archive_name, oid, archive_id) + f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}" ) raise ValueError("invalid archive name") return archive_id @@ -223,7 +226,7 @@ class BaseArchiver(object): + os.path.sep + "-".join(["%02d" % x for x in time.localtime()[:6]]) ) - log("creating archive: %s" % archive_id) + log(f"creating archive: {archive_id}") try: scu.GSL.acquire() os.mkdir(archive_id) # if exists, raises an OSError @@ -302,9 +305,14 @@ def do_formsemestre_archive( Store: - tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf) """ - from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet + from app.scodoc.sco_recapcomplet import ( + gen_formsemestre_recapcomplet_excel, + gen_formsemestre_recapcomplet_html, + gen_formsemestre_recapcomplet_json, + ) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem_archive_id = formsemestre_id archive_id = PVArchive.create_obj_archive(sem_archive_id, description) date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") @@ -319,37 +327,38 @@ def do_formsemestre_archive( etudids = [m["etudid"] for m in groups_infos.members] # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) - data, _, _ = make_formsemestre_recapcomplet(formsemestre_id, format="xls") + data, _ = gen_formsemestre_recapcomplet_excel( + formsemestre, res, include_evaluations=True, format="xls" + ) if data: PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) - data, _, _ = make_formsemestre_recapcomplet( - formsemestre_id, format="html", disable_etudlink=True + table_html = gen_formsemestre_recapcomplet_html( + formsemestre, res, include_evaluations=True ) - if data: + if table_html: data = "\n".join( [ html_sco_header.sco_header( - page_title="Moyennes archivées le %s" % date, - head_message="Moyennes archivées le %s" % date, + page_title=f"Moyennes archivées le {date}", + head_message=f"Moyennes archivées le {date}", no_side_bar=True, ), - '

Valeurs archivées le %s

' % date, + f'

Valeurs archivées le {date}

', '', - data, + table_html, html_sco_header.sco_footer(), ] ) data = data.encode(scu.SCO_ENCODING) PVArchive.store(archive_id, "Tableau_moyennes.html", data) - # Bulletins en XML (pour tous les etudiants, n'utilise pas les groupes) - data, _, _ = make_formsemestre_recapcomplet( - formsemestre_id, format="xml", xml_with_decisions=True - ) + # Bulletins en JSON + data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) + data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) + data_js = data_js.encode(scu.SCO_ENCODING) if data: - data = data.encode(scu.SCO_ENCODING) - PVArchive.store(archive_id, "Bulletins.xml", data) + PVArchive.store(archive_id, "Bulletins.json", data_js) # Decisions de jury, en XLS data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False) if data: diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 97824780..5d1b2d72 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -185,13 +185,13 @@ def excel_make_style( class ScoExcelSheet: - """Représente une feuille qui peut être indépendante ou intégrée dans un SCoExcelBook. + """Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook. En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations est imposé: * instructions globales (largeur/maquage des colonnes et ligne, ...) * construction et ajout des cellules et ligne selon le sens de lecture (occidental) ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..) - * pour finit appel de la méthode de génération + * pour finir appel de la méthode de génération """ def __init__(self, sheet_name="feuille", default_style=None, wb=None): @@ -260,7 +260,7 @@ class ScoExcelSheet: for i, val in enumerate(value): self.ws.column_dimensions[self.i2col(i)].width = val # No keys: value is a list of widths - elif type(cle) == str: # accepts set_column_with("D", ...) + elif isinstance(cle, str): # accepts set_column_with("D", ...) self.ws.column_dimensions[cle].width = value else: self.ws.column_dimensions[self.i2col(cle)].width = value @@ -337,7 +337,8 @@ class ScoExcelSheet: return cell - def make_row(self, values: list, style=None, comments=None): + def make_row(self, values: list, style=None, comments=None) -> list: + "build a row" # TODO make possible differents styles in a row if comments is None: comments = [None] * len(values) diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 96d0a737..ba517e6a 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -184,9 +184,6 @@ def formsemestre_validation_etud_form( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, modejury=1, - hidemodules=1, - hidebac=1, - pref_override=0, formsemestre_id=formsemestre_id, sortcol=sortcol or None, # pour refaire tri sorttable du tableau de notes @@ -437,14 +434,6 @@ def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol # sinon renvoie au listing general, -# if choice.new_code_prev: -# flask.redirect( 'formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1&desturl=%s' % (formsemestre_id, etudid, desturl) ) -# else: -# if not desturl: -# desturl = 'formsemestre_recapcomplet?modejury=1&hidemodules=1&formsemestre_id=' + str(formsemestre_id) -# flask.redirect(desturl) - - def _dispcode(c): if not c: return "" diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 7f1d8cc4..112b0da5 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -382,18 +382,6 @@ class BasePreferences(object): "only_global": False, }, ), - ( - "recap_hidebac", - { - "initvalue": 0, - "title": "Cacher la colonne Bac", - "explanation": "sur la table récapitulative", - "input_type": "boolcheckbox", - "category": "misc", - "labels": ["non", "oui"], - "only_global": False, - }, - ), # ------------------ Absences ( "email_chefdpt", diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 84fbafc3..72d16012 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -28,7 +28,6 @@ """Tableau récapitulatif des notes d'un semestre """ import datetime -import json import time from xml.etree import ElementTree @@ -43,6 +42,7 @@ from app.models import FormSemestre from app.models.etudiants import Identite from app.models.evaluations import Evaluation +from app.scodoc.gen_tables import GenTable import app.scodoc.sco_utils as scu from app.scodoc import html_sco_header from app.scodoc import sco_bulletins_json @@ -52,6 +52,7 @@ from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_status @@ -67,42 +68,38 @@ from app.scodoc.sco_codes_parcours import DEF, UE_SPORT def formsemestre_recapcomplet( formsemestre_id=None, modejury=False, # affiche lien saisie decision jury - hidemodules=False, # cache colonnes notes modules - hidebac=False, # cache colonne Bac tabformat="html", sortcol=None, xml_with_decisions=False, # XML avec decisions rank_partition_id=None, # si None, calcul rang global - pref_override=True, # si vrai, les prefs ont la priorite sur le param hidebac force_publishing=True, # publie les XML/JSON meme si bulletins non publiés ): """Page récapitulant les notes d'un semestre. Grand tableau récapitulatif avec toutes les notes de modules pour tous les étudiants, les moyennes par UE et générale, trié par moyenne générale décroissante. + + tabformat: + html : page web + evals : page web, avec toutes les évaluations dans le tableau + xls, xlsx: export excel simple + xlsall : export excel simple, avec toutes les évaluations dans le tableau + csv : export CSV, avec toutes les évaluations + xml, json : concaténation de tous les bulletins, au format demandé + pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable) + + modejury: cache modules, affiche lien saisie decision jury + """ - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] - parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - # Pour APC (BUT): cache les modules par défaut car moyenne n'a pas de sens - if formsemestre.formation.is_apc(): - hidemodules = True - # traduit du DTML + modejury = int(modejury) - hidemodules = ( - int(hidemodules) or parcours.UE_IS_MODULE - ) # cache les colonnes des modules - pref_override = int(pref_override) - if pref_override: - hidebac = int(sco_preferences.get_preference("recap_hidebac", formsemestre_id)) - else: - hidebac = int(hidebac) + xml_with_decisions = int(xml_with_decisions) force_publishing = int(force_publishing) - isFile = tabformat in ("csv", "xls", "xml", "xlsall", "json") + is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"} H = [] - if not isFile: + if not is_file: H += [ html_sco_header.sco_header( page_title="Récapitulatif", @@ -115,17 +112,14 @@ def formsemestre_recapcomplet( ), ] if len(formsemestre.inscriptions) > 0: - H += [ - '
' % request.base_url, - '' - % formsemestre_id, - '', - ] - + H.append( + f""" + + """ + ) if modejury: H.append( - '' - % modejury + f'' ) H.append( '") H.append( - f""" (cliquer sur un nom pour afficher son bulletin ou - ici avoir le classeur papier) + f""" (cliquer sur un nom pour afficher son bulletin ou ici avoir le classeur papier) """ ) data = do_formsemestre_recapcomplet( formsemestre_id, format=tabformat, - hidemodules=hidemodules, - hidebac=hidebac, modejury=modejury, sortcol=sortcol, xml_with_decisions=xml_with_decisions, @@ -168,24 +160,27 @@ def formsemestre_recapcomplet( return response H.append(data) - if not isFile: + if not is_file: if len(formsemestre.inscriptions) > 0: H.append("
") H.append( - """

Voir les décisions du jury

""" - % formsemestre_id + f"""

Voir les décisions du jury

""" ) if sco_permissions_check.can_validate_sem(formsemestre_id): H.append("

") if modejury: H.append( - """Calcul automatique des décisions du jury

""" - % (formsemestre_id,) + f"""Calcul automatique des décisions du jury

""" ) else: H.append( - """Saisie des décisions du jury""" - % formsemestre_id + f"""Saisie des décisions du jury""" ) H.append("

") if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): @@ -219,684 +214,59 @@ def do_formsemestre_recapcomplet( ): """Calcule et renvoie le tableau récapitulatif.""" formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + + filename = scu.sanitize_filename( + f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + if (format == "html" or format == "evals") and not modejury: res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - data, filename = gen_formsemestre_recapcomplet_html( - formsemestre, res, include_evaluations=(format == "evals") + data = gen_formsemestre_recapcomplet_html( + formsemestre, + res, + include_evaluations=(format == "evals"), + filename=filename, ) - else: - data, filename, format = make_formsemestre_recapcomplet( - formsemestre_id=formsemestre_id, - format=format, - hidemodules=hidemodules, - hidebac=hidebac, - xml_nodate=xml_nodate, - modejury=modejury, - sortcol=sortcol, - xml_with_decisions=xml_with_decisions, - disable_etudlink=disable_etudlink, - rank_partition_id=rank_partition_id, - force_publishing=force_publishing, - ) - # --- - if format == "xml" or format == "html" or format == "evals": return data - elif format == "csv": - return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE) - elif format.startswith("xls") or format.startswith("xlsx"): - return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE) - elif format == "json": - js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder) - return scu.send_file( - js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE + elif format.startswith("xls") or format == "csv": + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + include_evaluations = format in {"xlsall", "csv "} + if format != "csv": + format = "xlsx" + data, filename = gen_formsemestre_recapcomplet_excel( + formsemestre, + res, + include_evaluations=include_evaluations, + format=format, + filename=filename, ) - else: - raise ValueError(f"unknown format {format}") - - -def make_formsemestre_recapcomplet( - formsemestre_id=None, - format="html", # html, evals, xml, json - hidemodules=False, # ne pas montrer les modules (ignoré en XML) - hidebac=False, # pas de colonne Bac (ignoré en XML) - xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML) - modejury=False, # saisie décisions jury - sortcol=None, # indice colonne a trier dans table T - xml_with_decisions=False, - disable_etudlink=False, - rank_partition_id=None, # si None, calcul rang global - force_publishing=True, # donne bulletins JSON/XML meme si non publiés -): - """Grand tableau récapitulatif avec toutes les notes de modules - pour tous les étudiants, les moyennes par UE et générale, - trié par moyenne générale décroissante. - """ - civ_nom_prenom = False # 3 colonnes différentes ou une seule avec prénom abrégé ? - if format == "xml": - return _formsemestre_recapcomplet_xml( + return scu.send_file(data, filename=filename, mime=scu.get_mime_suffix(format)) + elif format == "xml": + data = gen_formsemestre_recapcomplet_xml( formsemestre_id, xml_nodate, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, ) + return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX) elif format == "json": - return _formsemestre_recapcomplet_json( + data = gen_formsemestre_recapcomplet_json( formsemestre_id, xml_nodate=xml_nodate, xml_with_decisions=xml_with_decisions, force_publishing=force_publishing, ) - if format[:3] == "xls": - civ_nom_prenom = True # 3 cols: civilite, nom, prenom - keep_numeric = True # pas de conversion des notes en strings - else: - keep_numeric = False + return scu.sendJSON(data, filename=filename) - if hidebac: - admission_extra_cols = [] - else: - admission_extra_cols = [ - "type_admission", - "classement", - "apb_groupe", - "apb_classement_gr", - ] - - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - # A ré-écrire XXX - sem = sco_formsemestre.do_formsemestre_list( - args={"formsemestre_id": formsemestre_id} - )[0] - parcours = formsemestre.formation.get_parcours() - - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - modimpls = formsemestre.modimpls_sorted - ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport - - partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups( - formsemestre_id - ) - if rank_partition_id and format == "html": - # Calcul rang sur une partition et non sur l'ensemble - # seulement en format HTML (car colonnes rangs toujours presentes en xls) - rank_partition = sco_groups.get_partition(rank_partition_id) - rank_label = "Rg (%s)" % rank_partition["partition_name"] - else: - rank_partition = sco_groups.get_default_partition(formsemestre_id) - rank_label = "Rg" - - T = nt.get_table_moyennes_triees() - if not T: - return "", "", format - - # Construit une liste de listes de chaines: le champs du tableau resultat (HTML ou CSV) - F = [] - h = [rank_label] - if civ_nom_prenom: - h += ["Civilité", "Nom", "Prénom"] - else: - h += ["Nom"] - if not hidebac: - h.append("Bac") - - # Si CSV ou XLS, indique tous les groupes - if format[:3] == "xls" or format == "csv": - for partition in partitions: - h.append("%s" % partition["partition_name"]) - else: - h.append("Gr") - - h.append("Moy") - # Ajoute rangs dans groupe seulement si CSV ou XLS - if format[:3] == "xls" or format == "csv": - for partition in partitions: - h.append("rang_%s" % partition["partition_name"]) - - cod2mod = {} # code : moduleimpl - mod_evals = {} # moduleimpl_id : liste de toutes les evals de ce module - for ue in ues: - if ue["type"] != UE_SPORT: - h.append(ue["acronyme"]) - else: # UE_SPORT: - # n'affiche pas la moyenne d'UE dans ce cas - # mais laisse col. vide si modules affichés (pour séparer les UE) - if not hidemodules: - h.append("") - pass - if not hidemodules and not ue["is_external"]: - for modimpl in modimpls: - if modimpl.module.ue_id == ue["ue_id"]: - code = modimpl.module.code - h.append(code) - cod2mod[code] = modimpl # pour fabriquer le lien - if format == "xlsall": - evals = nt.modimpls_results[ - modimpl.id - ].get_evaluations_completes(modimpl) - # evals = nt.get_mod_evaluation_etat_list(... - mod_evals[modimpl.id] = evals - h += _list_notes_evals_titles(code, evals) - - h += admission_extra_cols - h += ["code_nip", "etudid"] - F.append(h) - - def fmtnum(val): # conversion en nombre pour cellules excel - if keep_numeric: - try: - return float(val) - except: - return val - else: - return val - - # Compte les decisions de jury - codes_nb = scu.DictDefault(defaultvalue=0) - # - is_dem = {} # etudid : bool - for t in T: - etudid = t[-1] - dec = nt.get_etud_decision_sem(etudid) - if dec: - codes_nb[dec["code"]] += 1 - etud_etat = nt.get_etud_etat(etudid) - if etud_etat == "D": - gr_name = "Dém." - is_dem[etudid] = True - elif etud_etat == DEF: - gr_name = "Déf." - is_dem[etudid] = False - else: - group = sco_groups.get_etud_main_group(etudid, formsemestre_id) - gr_name = group["group_name"] or "" - is_dem[etudid] = False - if rank_partition_id: - rang_gr, _, rank_gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt - ) - if rank_gr_name[rank_partition_id]: - rank = "%s %s" % ( - rank_gr_name[rank_partition_id], - rang_gr[rank_partition_id], - ) - else: - rank = "" - else: - rank = nt.get_etud_rang(etudid) - - e = nt.identdict[etudid] - if civ_nom_prenom: - sco_etud.format_etud_ident(e) - l = [rank, e["civilite_str"], e["nom_disp"], e["prenom"]] # civ, nom prenom - else: - l = [rank, nt.get_nom_short(etudid)] # rang, nom, - - e["admission"] = {} - if not hidebac: - e["admission"] = nt.etuds_dict[etudid].admission.first() - if e["admission"]: - bac = nt.etuds_dict[etudid].admission[0].get_bac() - l.append(bac.abbrev()) - else: - l.append("") - - if format[:3] == "xls" or format == "csv": # tous les groupes - for partition in partitions: - group = partitions_etud_groups[partition["partition_id"]].get( - etudid, None - ) - if group: - l.append(group["group_name"]) - else: - l.append("") - else: - l.append(gr_name) # groupe - - # Moyenne générale - l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric))) - # Ajoute rangs dans groupes seulement si CSV ou XLS - if format[:3] == "xls" or format == "csv": - rang_gr, _, gr_name = sco_bulletins.get_etud_rangs_groups( - etudid, formsemestre_id, partitions, partitions_etud_groups, nt - ) - - for partition in partitions: - l.append(rang_gr[partition["partition_id"]]) - - # Nombre d'UE au dessus de 10 - # t[i] est une chaine :-) - # nb_ue_ok = sum( - # [t[i] > 10 for i, ue in enumerate(ues, start=1) if ue["type"] != UE_SPORT] - # ) - ue_index = [] # indices des moy UE dans l (pour appliquer style css) - for i, ue in enumerate(ues, start=1): - if ue["type"] != UE_SPORT: - l.append( - fmtnum(scu.fmt_note(t[i], keep_numeric=keep_numeric)) - ) # moyenne etud dans ue - else: # UE_SPORT: - # n'affiche pas la moyenne d'UE dans ce cas - if not hidemodules: - l.append("") - ue_index.append(len(l) - 1) - if not hidemodules and not ue["is_external"]: - j = 0 - for modimpl in modimpls: - if modimpl.module.ue_id == ue["ue_id"]: - l.append( - fmtnum( - scu.fmt_note( - t[j + len(ues) + 1], keep_numeric=keep_numeric - ) - ) - ) # moyenne etud dans module - if format == "xlsall": - l += _list_notes_evals(mod_evals[modimpl.id], etudid) - j += 1 - if not hidebac: - for k in admission_extra_cols: - l.append(getattr(e["admission"], k, "") or "") - l.append( - nt.identdict[etudid]["code_nip"] or "" - ) # avant-derniere colonne = code_nip - l.append(etudid) # derniere colonne = etudid - F.append(l) - - # Dernière ligne: moyennes, min et max des UEs et modules - if not hidemodules: # moy/min/max dans chaque module - mods_stats = {} # moduleimpl_id : stats - for modimpl in modimpls: - mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id) - - def add_bottom_stat(key, title, corner_value=""): - l = ["", title] - if civ_nom_prenom: - l += ["", ""] - if not hidebac: - l.append("") - if format[:3] == "xls" or format == "csv": - l += [""] * len(partitions) - else: - l += [""] - l.append(corner_value) - if format[:3] == "xls" or format == "csv": - for _ in partitions: - l += [""] # rangs dans les groupes - for ue in ues: - if ue["type"] != UE_SPORT: - if key == "nb_valid_evals": - l.append("") - elif key == "coef": - if sco_preferences.get_preference("use_ue_coefs", formsemestre_id): - l.append("%2.3f" % ue["coefficient"]) - else: - l.append("") - else: - if key == "ects": - if keep_numeric: - l.append(ue[key]) - else: - l.append(str(ue[key])) - else: - l.append(scu.fmt_note(ue[key], keep_numeric=keep_numeric)) - else: # UE_SPORT: - # n'affiche pas la moyenne d'UE dans ce cas - if not hidemodules: - l.append("") - # ue_index.append(len(l) - 1) - if not hidemodules and not ue["is_external"]: - for modimpl in modimpls: - if modimpl.module.ue_id == ue["ue_id"]: - if key == "coef": - coef = modimpl.module.coefficient - if format[:3] != "xls": - coef = str(coef) - l.append(coef) - elif key == "ects": - l.append("") # ECTS module ? - else: - val = mods_stats[modimpl.id][key] - if key == "nb_valid_evals": - if ( - format[:3] != "xls" - ): # garde val numerique pour excel - val = str(val) - else: # moyenne du module - val = scu.fmt_note(val, keep_numeric=keep_numeric) - l.append(val) - - if format == "xlsall": - l += _list_notes_evals_stats(mod_evals[modimpl.id], key) - if modejury: - l.append("") # case vide sur ligne "Moyennes" - - l += [""] * len(admission_extra_cols) # infos admission vides ici - F.append(l + ["", ""]) # ajoute cellules code_nip et etudid inutilisees ici - - add_bottom_stat( - "min", "Min", corner_value=scu.fmt_note(nt.moy_min, keep_numeric=keep_numeric) - ) - add_bottom_stat( - "max", "Max", corner_value=scu.fmt_note(nt.moy_max, keep_numeric=keep_numeric) - ) - add_bottom_stat( - "moy", - "Moyennes", - corner_value=scu.fmt_note(nt.moy_moy, keep_numeric=keep_numeric), - ) - add_bottom_stat("coef", "Coef") - add_bottom_stat("nb_valid_evals", "Nb évals") - add_bottom_stat("ects", "ECTS") - - # Génération de la table au format demandé - if format == "html": - # Table format HTML - H = [ - """ - - - """ - ] - if sortcol: # sort table using JS sorttable - H.append( - """ - """ - % (int(sortcol)) - ) - - ligne_titres_head = _ligne_titres( - ue_index, F, cod2mod, modejury, with_modules_links=False - ) - ligne_titres_foot = _ligne_titres( - ue_index, F, cod2mod, modejury, with_modules_links=True - ) - - H.append("\n" + ligne_titres_head + "\n\n\n") - if disable_etudlink: - etudlink = "%(name)s" - else: - etudlink = """%(name)s""" - ir = 0 - nblines = len(F) - 1 - for l in F[1:]: - etudid = l[-1] - if ir == nblines - 6: - H.append("") - H.append("") - if ir >= nblines - 6: - # dernieres lignes: - el = l[1] - styl = ( - "recap_row_min", - "recap_row_max", - "recap_row_moy", - "recap_row_coef", - "recap_row_nbeval", - "recap_row_ects", - )[ir - nblines + 6] - cells = f'' - else: - el = etudlink % { - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "name": l[1], - } - if ir % 2 == 0: - cells = f'' - else: - cells = f'' - ir += 1 - # XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ] - # notes sans le NA: - nsn = l[:-2] # copy - for i, _ in enumerate(nsn): - if nsn[i] == "NA": - nsn[i] = "-" - try: - order = int(nsn[0].split()[0]) - except: - order = 99999 - cells += ( - f'' # rang - ) - cells += '' % el # nom etud (lien) - if not hidebac: - cells += '' % nsn[2] # bac - idx_col_gr = 3 - else: - idx_col_gr = 2 - cells += '' % nsn[idx_col_gr] # group name - - # Style si moyenne generale < barre - idx_col_moy = idx_col_gr + 1 - cssclass = "recap_col_moy" - try: - if float(nsn[idx_col_moy]) < (parcours.BARRE_MOY - scu.NOTES_TOLERANCE): - cssclass = "recap_col_moy_inf" - except: - pass - cells += '' % (cssclass, nsn[idx_col_moy]) - ue_number = 0 - for i in range(idx_col_moy + 1, len(nsn)): - if i in ue_index: - cssclass = "recap_col_ue" - # grise si moy UE < barre - ue = ues[ue_number] - ue_number += 1 - - if (ir < (nblines - 4)) or (ir == nblines - 3): - try: - if float(nsn[i]) < parcours.get_barre_ue( - ue["type"] - ): # NOTES_BARRE_UE - cssclass = "recap_col_ue_inf" - elif float(nsn[i]) >= parcours.NOTES_BARRE_VALID_UE: - cssclass = "recap_col_ue_val" - except: - pass - else: - cssclass = "recap_col" - if ( - ir == nblines - 3 - ): # si moyenne generale module < barre ue, surligne: - try: - if float(nsn[i]) < parcours.get_barre_ue(ue["type"]): - cssclass = "recap_col_moy_inf" - except: - pass - cells += '' % (cssclass, nsn[i]) - if modejury and etudid: - decision_sem = nt.get_etud_decision_sem(etudid) - if is_dem[etudid]: - code = "DEM" - act = "" - elif decision_sem: - code = decision_sem["code"] - act = "(modifier)" - else: - code = "" - act = "saisir" - cells += '" - H.append(cells + "") - - H.append(ligne_titres_foot) - H.append("") - H.append("
{nsn[0]}%s%s%s%s%s%s' % code - if act: - # cells += ' %s' % (formsemestre_id, etudid, act) - cells += ( - """ %s""" - % (formsemestre_id, etudid, act) - ) - cells += "
") - - # Form pour choisir partition de classement: - if not modejury and partitions: - H.append("Afficher le rang des groupes de: ") - if not rank_partition_id: - checked = "checked" - else: - checked = "" - H.append( - 'tous ' - % (checked) - ) - for p in partitions: - if p["partition_id"] == rank_partition_id: - checked = "checked" - else: - checked = "" - H.append( - '%s ' - % (p["partition_id"], checked, p["partition_name"]) - ) - - # recap des decisions jury (nombre dans chaque code): - if codes_nb: - H.append("

Décisions du jury

") - cods = list(codes_nb.keys()) - cods.sort() - for cod in cods: - H.append("" % (cod, codes_nb[cod])) - H.append("
%s%d
") - # Avertissements - if formsemestre.formation.is_apc(): - H.append( - """

Pour les formations par compétences (comme le BUT), la moyenne générale est purement indicative et ne devrait pas être communiquée aux étudiants.

""" - ) - return "\n".join(H), "", "html" - elif format == "csv": - CSV = scu.CSV_LINESEP.join( - [scu.CSV_FIELDSEP.join([str(x) for x in l]) for l in F] - ) - semname = sem["titre_num"].replace(" ", "_") - date = time.strftime("%d-%m-%Y") - filename = "notes_modules-%s-%s.csv" % (semname, date) - return CSV, filename, "csv" - elif format[:3] == "xls": - semname = sem["titre_num"].replace(" ", "_") - date = time.strftime("%d-%m-%Y") - if format == "xls": - filename = "notes_modules-%s-%s%s" % (semname, date, scu.XLSX_SUFFIX) - else: - filename = "notes_modules_evals-%s-%s%s" % (semname, date, scu.XLSX_SUFFIX) - sheet_name = "notes %s %s" % (semname, date) - if len(sheet_name) > 31: - sheet_name = "notes %s %s" % ("...", date) - xls = sco_excel.excel_simple_table( - titles=["etudid", "code_nip"] + F[0][:-2], - lines=[ - [x[-1], x[-2]] + x[:-2] for x in F[1:] - ], # reordonne cols (etudid et nip en 1er), - sheet_name=sheet_name, - ) - return xls, filename, "xls" - else: - raise ValueError("unknown format %s" % format) + raise ScoValueError(f"Format demandé invalide: {format}") -def _ligne_titres(ue_index, F, cod2mod, modejury, with_modules_links=True): - """Cellules de la ligne de titre (haut ou bas)""" - cells = '' - for i in range(len(F[0]) - 2): - if i in ue_index: - cls = "recap_tit_ue" - else: - cls = "recap_tit" - attr = f'class="{cls}"' - if i == 0 or F[0][i] == "classement": # Rang: force tri numerique - try: - order = int(F[0][i].split()[0]) - except: - order = 99999 - attr += f' data-order="{order:05d}"' - if F[0][i] in cod2mod: # lien vers etat module - modimpl = cod2mod[F[0][i]] - if with_modules_links: - href = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - else: - href = "" - cells += f"""{F[0][i]}""" - else: - cells += f"{F[0][i]}" - if modejury: - cells += 'Décision' - return cells + "" - - -def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]: - """Liste des notes des evaluations completes de ce module - (pour table xls avec evals) - """ - L = [] - for e in evals: - notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id) - 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 - val_fmt = scu.fmt_note(val, keep_numeric=True) - L.append(val_fmt) - return L - - -def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]: - """Liste des titres des evals completes""" - L = [] - eval_index = len(evals) - 1 - for e in evals: - L.append( - codemodule - + "-" - + str(eval_index) - + "-" - + (e.jour.isoformat() if e.jour else "") - ) - eval_index -= 1 - return L - - -def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]: - """Liste des stats (moy, ou rien!) des evals completes""" - L = [] - for e in evals: - if key == "moy": - # TODO #sco92 - # val = e["etat"]["moy_num"] - # L.append(scu.fmt_note(val, keep_numeric=True)) - L.append("") - elif key == "max": - L.append(e.note_max) - elif key == "min": - L.append(0.0) - elif key == "coef": - L.append(e.coefficient) - else: - L.append("") # on n'a pas sous la main min/max - return L - - -def _formsemestre_recapcomplet_xml( +def gen_formsemestre_recapcomplet_xml( formsemestre_id, xml_nodate, xml_with_decisions=False, force_publishing=True, -): +) -> str: "XML export: liste tous les bulletins XML." formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) @@ -931,23 +301,19 @@ def _formsemestre_recapcomplet_xml( xml_nodate=xml_nodate, xml_with_decisions=xml_with_decisions, ) - return ( - sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING), - "", - "xml", - ) + return ElementTree.tostring(doc).decode(scu.SCO_ENCODING) -def _formsemestre_recapcomplet_json( +def gen_formsemestre_recapcomplet_json( formsemestre_id, xml_nodate=False, xml_with_decisions=False, force_publishing=True, -): +) -> dict: """JSON export: liste tous les bulletins JSON :param xml_nodate(bool): indique la date courante (attribut docdate) :param force_publishing: donne les bulletins même si non "publiés sur portail" - :returns: dict, "", "json" + :returns: dict """ formsemestre = FormSemestre.query.get_or_404(formsemestre_id) is_apc = formsemestre.formation.is_apc() @@ -957,7 +323,7 @@ def _formsemestre_recapcomplet_json( else: docdate = datetime.datetime.now().isoformat() evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id) - J = { + js_data = { "docdate": docdate, "formsemestre_id": formsemestre_id, "evals_info": { @@ -968,7 +334,7 @@ def _formsemestre_recapcomplet_json( }, "bulletins": [], } - bulletins = J["bulletins"] + bulletins = js_data["bulletins"] formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) T = nt.get_table_moyennes_triees() @@ -986,7 +352,7 @@ def _formsemestre_recapcomplet_json( xml_with_decisions=xml_with_decisions, ) bulletins.append(bul) - return J, "", "json" + return js_data def formsemestres_bulletins(annee_scolaire): @@ -994,16 +360,16 @@ def formsemestres_bulletins(annee_scolaire): :param annee_scolaire(int): année de début de l'année scolaire :returns: JSON """ - jslist = [] + js_list = [] sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire) log("formsemestres_bulletins(%s): %d sems" % (annee_scolaire, len(sems))) for sem in sems: - J, _, _ = _formsemestre_recapcomplet_json( + js_data = gen_formsemestre_recapcomplet_json( sem["formsemestre_id"], force_publishing=False ) - jslist.append(J) + js_list.append(js_data) - return scu.sendJSON(jslist) + return scu.sendJSON(js_list) def _gen_cell(key: str, row: dict, elt="td"): @@ -1029,15 +395,16 @@ def _gen_row(keys: list[str], row, elt="td"): def gen_formsemestre_recapcomplet_html( - formsemestre: FormSemestre, res: NotesTableCompat, include_evaluations=False + formsemestre: FormSemestre, + res: NotesTableCompat, + include_evaluations=False, + filename="", ): """Construit table recap pour le BUT Cache le résultat pour le semestre. Return: data, filename + data est une chaine, le
...
incluant le tableau. """ - filename = scu.sanitize_filename( - f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" - ) if include_evaluations: table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id) else: @@ -1051,7 +418,7 @@ def gen_formsemestre_recapcomplet_html( else: sco_cache.TableRecapCache.set(formsemestre.id, table_html) - return table_html, filename + return table_html def _gen_formsemestre_recapcomplet_html( @@ -1066,8 +433,7 @@ def _gen_formsemestre_recapcomplet_html( ) if not rows: return ( - '
aucun étudiant !
', - "", + '
aucun étudiant !
' ) H = [ f"""
tuple: + """Génère le tableau recap en excel (xlsx) ou CSV. + Utilisé pour archives et autres besoins particuliers (API). + Attention: le tableau exporté depuis la page html est celui généré en js par DataTables, + et non celui-ci. + """ + suffix = scu.CSV_SUFFIX if format == "csv" else scu.XLSX_SUFFIX + filename += suffix + rows, footer_rows, titles, column_ids = res.get_table_recap( + convert_values=False, include_evaluations=include_evaluations + ) + + tab = GenTable( + columns_ids=column_ids, + titles=titles, + rows=rows + footer_rows, + preferences=sco_preferences.SemPreferences(formsemestre_id=formsemestre.id), + ) + + return tab.gen(format=format), filename diff --git a/sco_version.py b/sco_version.py index aced26cf..4e9e9ad1 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.91" +SCOVERSION = "9.1.92" SCONAME = "ScoDoc"