From 2ffcc503cb4e773a601f680f818fc926d2454b3a Mon Sep 17 00:00:00 2001 From: ivan Date: Sun, 16 Apr 2023 05:32:21 +0200 Subject: [PATCH] =?UTF-8?q?Apog=C3=A9e:=20-=20corrige=20affichage=20=C3=A9?= =?UTF-8?q?tat=20(filtres/comptes)=20-=20affichage=20et=20modification=20p?= =?UTF-8?q?=C3=A9riode=20semset=20(+=20API).=20-=20export=20r=C3=A9sultat?= =?UTF-8?q?=20annuel=20BUT.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/__init__.py | 1 + app/api/semset.py | 39 +++++++ app/models/formsemestre.py | 11 +- app/scodoc/gen_tables.py | 16 +-- app/scodoc/sco_apogee_csv.py | 205 +++++++++++++++++++++------------- app/scodoc/sco_etape_bilan.py | 103 +++++++++-------- app/scodoc/sco_semset.py | 37 ++++-- 7 files changed, 268 insertions(+), 144 deletions(-) create mode 100644 app/api/semset.py diff --git a/app/api/__init__.py b/app/api/__init__.py index 907447037..d5b436881 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -46,5 +46,6 @@ from app.api import ( jury, logos, partitions, + semset, users, ) diff --git a/app/api/semset.py b/app/api/semset.py new file mode 100644 index 000000000..f1239534f --- /dev/null +++ b/app/api/semset.py @@ -0,0 +1,39 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +""" + ScoDoc 9 API : accès aux formsemestres +""" +from flask import g, jsonify, request +from flask_login import login_required + +import app +from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR +from app.decorators import scodoc, permission_required +from app.scodoc.sco_utils import json_error +from app.models.formsemestre import NotesSemSet +from app.scodoc.sco_permissions import Permission + + +@bp.route("/semset/set_periode/", methods=["POST"]) +@api_web_bp.route("/semset/set_periode/", methods=["POST"]) +@login_required +@scodoc +@permission_required(Permission.ScoEditApo) +# TODO à modifier pour utiliser @as_json +def semset_set_periode(semset_id: int): + "Change la période d'un semset" + query = NotesSemSet.query.filter_by(semset_id=semset_id) + if g.scodoc_dept: + query = query.filter_by(dept_id=g.scodoc_dept_id) + semset: NotesSemSet = query.first_or_404() + data = request.get_json(force=True) # may raise 400 Bad Request + try: + periode = int(data) + semset.set_periode(periode) + except ValueError: + return json_error(API_CLIENT_ERROR, "invalid periode value") + return jsonify({"OK": True}) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index d48b6b152..0ec4e6be3 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -967,7 +967,7 @@ class FormationModalite(db.Model): """Create default modalities""" numero = 0 try: - for (code, titre) in ( + for code, titre in ( (FormationModalite.DEFAULT_MODALITE, "Formation Initiale"), ("FAP", "Apprentissage"), ("FC", "Formation Continue"), @@ -1108,6 +1108,15 @@ class NotesSemSet(db.Model): sem_id = db.Column(db.Integer, nullable=False, default=0) "période: 0 (année), 1 (Simpair), 2 (Spair)" + def set_periode(self, periode: int): + """Modifie la période 0 (année), 1 (Simpair), 2 (Spair)""" + if periode not in {0, 1, 2}: + raise ValueError("periode invalide") + self.sem_id = periode + log(f"semset.set_periode({self.id}, {periode})") + db.session.add(self) + db.session.commit() + # Association: many to many notes_semset_formsemestre = db.Table( diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index d15532389..5ffb5a71d 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -758,31 +758,31 @@ class SeqGenTable(object): def excel(self): """Export des genTables dans un unique fichier excel avec plusieurs feuilles tagguées""" book = sco_excel.ScoExcelBook() # pylint: disable=no-member - for (_, gt) in self.genTables.items(): + for _, gt in self.genTables.items(): gt.excel(wb=book) # Ecrit dans un fichier excel return book.generate() # ----- Exemple d'utilisation minimal. if __name__ == "__main__": - T = GenTable( + table = GenTable( rows=[{"nom": "Hélène", "age": 26}, {"nom": "Titi&çà§", "age": 21}], columns_ids=("nom", "age"), ) print("--- HTML:") - print(T.gen(format="html")) + print(table.gen(format="html")) print("\n--- XML:") - print(T.gen(format="xml")) + print(table.gen(format="xml")) print("\n--- JSON:") - print(T.gen(format="json")) + print(table.gen(format="json")) # Test pdf: import io from reportlab.platypus import KeepInFrame from app.scodoc import sco_preferences, sco_pdf preferences = sco_preferences.SemPreferences() - T.preferences = preferences - objects = T.gen(format="pdf") + table.preferences = preferences + objects = table.gen(format="pdf") objects = [KeepInFrame(0, 0, objects, mode="shrink")] doc = io.BytesIO() document = sco_pdf.BaseDocTemplate(doc) @@ -795,6 +795,6 @@ if __name__ == "__main__": data = doc.getvalue() with open("/tmp/gen_table.pdf", "wb") as f: f.write(data) - p = T.make_page(format="pdf") + p = table.make_page(format="pdf") with open("toto.pdf", "wb") as f: f.write(p) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index a19dca83c..64ca9a355 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -100,7 +100,8 @@ from chardet import detect as chardet_detect from app import log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, Identite +from app.comp.res_but import ResultatsSemestreBUT +from app.models import FormSemestre, Identite, ApcValidationAnnee from app.models.config import ScoDocSiteConfig import app.scodoc.sco_utils as scu from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError @@ -220,10 +221,10 @@ class ApoElt(object): self.cols.append(col) def __repr__(self): - return "ApoElt(code='%s', cols=%s)" % (self.code, pprint.pformat(self.cols)) + return f"ApoElt(code='{self.code}', cols={pprint.pformat(self.cols)})" -class EtuCol(object): +class EtuCol: """Valeurs colonnes d'un element pour un etudiant""" def __init__(self, nip, apo_elt, init_vals): @@ -276,7 +277,8 @@ class ApoEtud(dict): self.export_res_sem = export_res_sem # elt_sem_apo self.export_res_ues = export_res_ues self.export_res_modules = export_res_modules - self.export_res_sdj = export_res_sdj # export meme si pas de decision de jury + self.export_res_sdj = export_res_sdj + "export meme si pas de decision de jury" self.export_res_rat = export_res_rat self.fmt_note = functools.partial( _apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f" @@ -354,7 +356,8 @@ class ApoEtud(dict): ] except KeyError as exc: log( - f"associate_sco: missing key, etud={self}\ncode='{code}'\netape='{apo_data.etape_apogee}'" + f"""associate_sco: missing key, etud={self}\ncode='{ + code}'\netape='{apo_data.etape_apogee}'""" ) raise ScoValueError( f"""L'élément {code} n'a pas de résultat: peut-être une erreur @@ -372,7 +375,7 @@ class ApoEtud(dict): def search_elt_in_sem(self, code, sem, cur_sem, autre_sem) -> dict: """ - VET code jury etape + VET code jury etape (en BUT, le code annuel) ELP élément pédagogique: UE, module Autres éléments: résultats du semestre ou de l'année scolaire: => VRTW1: code additionnel au semestre ("code élement semestre", elt_sem_apo) @@ -401,7 +404,7 @@ class ApoEtud(dict): # pas de decision de jury, on n'enregistre rien # (meme si démissionnaire) if not self.has_logged_no_decision: - self.log.append("Pas de decision") + self.log.append("Pas de décision (export désactivé)") self.has_logged_no_decision = True return VOID_APO_RES @@ -423,6 +426,7 @@ class ApoEtud(dict): if export_res_etape: return self.comp_elt_annuel(etudid, cur_sem, autre_sem) else: + self.log.append("export étape désactivé") return VOID_APO_RES # Element semestre: @@ -495,43 +499,53 @@ class ApoEtud(dict): def comp_elt_annuel(self, etudid, cur_sem, autre_sem): """Calcul resultat annuel (VET) à partir du semestre courant et de l'autre (le suivant ou le précédent complétant l'année scolaire) + En BUT, c'est la décision de jury annuelle (ApcValidationAnnee). """ # Code annuel: - # - Note: moyenne des moyennes générales des deux semestres (pas vraiment de sens, mais faute de mieux) - # on pourrait aussi bien prendre seulement la note du dernier semestre (S2 ou S4). Paramétrable ? + # - Note: moyenne des moyennes générales des deux semestres + # (pas vraiment de sens, mais faute de mieux) + # on pourrait aussi bien prendre seulement la note du dernier semestre (S2 ou S4). + # XXX APOBUT: à modifier pour prendre moyenne indicative annuelle + # # - Résultat jury: # si l'autre est validé, code du semestre courant (ex: S1 (ADM), S2 (AJ) => année AJ) # si l'autre n'est pas validé ou est DEF ou DEM, code de l'autre # # XXX cette règle est discutable, à valider - # print 'comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id']) + # log('comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id'])) if not cur_sem: # l'étudiant n'a pas de semestre courant ?! + self.log.append("pas de semestre courant") log(f"comp_elt_annuel: etudid {etudid} has no cur_sem") return VOID_APO_RES cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"]) cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre) - cur_decision = cur_nt.get_etud_decision_sem(etudid) - if not cur_decision: - # pas de decision => pas de résultat annuel - return VOID_APO_RES - if (cur_decision["code"] == RAT) and not self.export_res_rat: - # ne touche pas aux RATs - return VOID_APO_RES + if not self.is_apc: + cur_decision = cur_nt.get_etud_decision_sem(etudid) + if not cur_decision: + # pas de decision => pas de résultat annuel + return VOID_APO_RES + + if (cur_decision["code"] == RAT) and not self.export_res_rat: + # ne touche pas aux RATs + return VOID_APO_RES if not autre_sem: # formations monosemestre, ou code VET semestriel, # ou jury intermediaire et etudiant non redoublant... return self.comp_elt_semestre(cur_nt, cur_decision, etudid) - decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"]) - autre_formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"]) autre_nt: NotesTableCompat = res_sem.load_formsemestre_results( autre_formsemestre ) + # --- Traite le BUT à part: + if self.is_apc: + return self.comp_elt_annuel_apc(cur_nt, autre_nt, etudid) + # --- Formations classiques + decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"]) autre_decision = autre_nt.get_etud_decision_sem(etudid) if not autre_decision: # pas de decision dans l'autre => pas de résultat annuel @@ -564,6 +578,38 @@ class ApoEtud(dict): return dict(N=note_str, B=20, J="", R=decision_apo_annuelle, M="") + def comp_elt_annuel_apc( + self, + cur_res: ResultatsSemestreBUT, + autre_res: ResultatsSemestreBUT, + etudid: int, + ): + """L'élément Apo pour un résultat annuel BUT. + cur_res : les résultats du semestre sur lequel a été appelé l'export. + """ + # le semestre impair de l'année scolaire + if cur_res.formsemestre.semestre_id % 2: + formsemestre = cur_res.formsemestre + elif ( + autre_res + and autre_res.formsemestre.annee_scolaire() + == cur_res.formsemestre.annee_scolaire() + ): + formsemestre = autre_res.formsemestre + assert formsemestre.semestre_id % 2 + else: + # ne trouve pas de semestre impair + return VOID_APO_RES + + validation: ApcValidationAnnee = ApcValidationAnnee.query.filter_by( + formsemestre_id=formsemestre.id, etudid=etudid + ).first() + if validation is None: + return VOID_APO_RES + return dict( + N="", B=20, J="", R=ScoDocSiteConfig.get_code_apo(validation.code), M="" + ) + def etud_semestres_de_etape(self, apo_data): """ Lorsqu'on a une formation semestrialisée mais avec un code étape annuel, @@ -599,12 +645,12 @@ class ApoEtud(dict): for sem in cur_sems: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - decision = nt.get_etud_decision_sem(self.etud["etudid"]) - if decision: + has_decision = nt.etud_has_decision(self.etud["etudid"]) + if has_decision: cur_sem = sem break if cur_sem is None: - cur_sem = cur_sems[0] # aucun avec decison, prend le plus recent + cur_sem = cur_sems[0] # aucun avec décision, prend le plus recent if apo_data.cur_semestre_id <= 0: return ( @@ -670,7 +716,7 @@ class ApoEtud(dict): return cur_sem, autre_sem -class ApoData(object): +class ApoData: def __init__( self, data: str, @@ -697,8 +743,13 @@ class ApoData(object): self.export_res_rat = export_res_rat self.orig_filename = orig_filename self.periode = periode # + "1 sem. sept-jan, 2 sem. fev-jul. 0 si étape en 1 seul semestre." self.is_apc = None "Vrai si BUT" + self.header: str = "" + "début du fichier Apogée (sera ré-écrit non modifié)" + self.titles: dict[str, str] = {} + "titres Apogée (section XX-APO_TITRES-XX)" try: self.read_csv(data) except ScoFormatError as e: @@ -723,13 +774,7 @@ class ApoData(object): False # True si jury à mi-étape, eg jury de S1 dans l'étape (S1, S2) ) - log( - "ApoData( periode=%s, annee_scolaire=%s )" - % (self.periode, self.annee_scolaire) - ) - - def set_periode(self, periode): # currently unused - self.periode = periode + log(f"ApoData( periode={self.periode}, annee_scolaire={self.annee_scolaire} )") def setup(self): """Recherche semestres ScoDoc concernés""" @@ -871,16 +916,16 @@ class ApoData(object): if not line.strip(): continue # silently ignore blank lines line = line.strip(APO_NEWLINE) - fs = line.split(APO_SEP) + fields = line.split(APO_SEP) cols = {} # { col_id : value } - for i in range(len(fs)): - cols[self.col_ids[i]] = fs[i] + for i, field in enumerate(fields): + cols[self.col_ids[i]] = field L.append( ApoEtud( - nip=fs[0], # id etudiant - nom=fs[1], - prenom=fs[2], - naissance=fs[3], + nip=fields[0], # id etudiant + nom=fields[1], + prenom=fields[2], + naissance=fields[3], cols=cols, export_res_etape=self.export_res_etape, export_res_sem=self.export_res_sem, @@ -902,7 +947,8 @@ class ApoData(object): def get_vdi_apogee(self): """le VDI (version de diplôme), stocké dans l'élément VET - (note: on pourrait peut-être aussi bien le récupérer dans l'en-tête XX-APO_TITRES-XX apoC_cod_vdi) + (note: on pourrait peut-être aussi bien le récupérer dans + l'en-tête XX-APO_TITRES-XX apoC_cod_vdi) """ for elt in self.apo_elts.values(): if elt.type_objet == "VET": @@ -923,7 +969,7 @@ class ApoData(object): m = re.match("[12][0-9]{3}", self.titles["apoC_annee"]) if not m: raise ScoFormatError( - 'Annee scolaire (apoC_annee) invalide: "%s"' % self.titles["apoC_annee"] + f"""Annee scolaire (apoC_annee) invalide: "{self.titles["apoC_annee"]}" """ ) return int(m.group(0)) @@ -939,10 +985,10 @@ class ApoData(object): def write_etuds(self, f): """write apo CSV etuds on f""" for e in self.etuds: - fs = [] # e['nip'], e['nom'], e['prenom'], e['naissance'] ] + fields = [] # e['nip'], e['nom'], e['prenom'], e['naissance'] ] for col_id in self.col_ids: try: - fs.append(str(e.new_cols[col_id])) + fields.append(str(e.new_cols[col_id])) except KeyError: log( "Error: %s %s missing column key %s" @@ -952,19 +998,18 @@ class ApoData(object): log("col_ids=%s" % pprint.pformat(self.col_ids)) log("etudiant ignore.\n") - f.write(APO_SEP.join(fs) + APO_NEWLINE) + f.write(APO_SEP.join(fields) + APO_NEWLINE) - def list_unknown_elements(self): + def list_unknown_elements(self) -> list[str]: """Liste des codes des elements Apogee non trouvés dans ScoDoc (après traitement de tous les étudiants) """ - s = set() + codes = set() for e in self.etuds: - ul = [code for code in e.col_elts if e.col_elts[code] is None] - s.update(ul) - L = list(s) - L.sort() - return L + codes.update({code for code in e.col_elts if e.col_elts[code] is None}) + codes_list = list(codes) + codes_list.sort() + return codes_list def list_elements(self): """Liste les codes des elements Apogée de la maquette @@ -978,19 +1023,18 @@ class ApoData(object): declared = self.col_ids[4:] # id des colones dans l'en-tête present = sorted(self.cols.keys()) # colones presentes log("Fichier Apogee invalide:") - log("Colonnes declarees: %s" % declared) - log("Colonnes presentes: %s" % present) + log(f"Colonnes declarees: {declared}") + log(f"Colonnes presentes: {present}") raise ScoFormatError( - """Fichier Apogee invalide
Colonnes declarees: %s -
Colonnes presentes: %s""" - % (declared, present) + f"""Fichier Apogee invalide
Colonnes declarees: {declared} +
Colonnes presentes: {present}""" ) # l'ensemble de tous les codes des elements apo des semestres: sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set()) return maq_elems, sem_elems - def get_codes_by_sem(self): + def get_codes_by_sem(self) -> dict[int, set[str]]: """Pour chaque semestre associé, donne l'ensemble des codes de cette maquette Apogée qui s'y trouvent (dans le semestre, les UE ou les modules). Return: { formsemestre_id : { 'code1', 'code2', ... }} @@ -1043,7 +1087,7 @@ class ApoData(object): "est_NAR": e.is_NAR, "commentaire": "; ".join(e.log), } - if e.col_elts and e.col_elts[self.etape_apogee] != None: + if e.col_elts and e.col_elts[self.etape_apogee] is not None: cr["etape"] = e.col_elts[self.etape_apogee].get("R", "") cr["etape_note"] = e.col_elts[self.etape_apogee].get("N", "") else: @@ -1076,10 +1120,10 @@ def _apo_read_cols(f): Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... } """ line = f.readline().strip(" " + APO_NEWLINE) - fs = line.split(APO_SEP) - if fs[0] != "apoL_a01_code": - raise ScoFormatError("invalid line: %s (expecting apoL_a01_code)" % line) - col_keys = fs + fields = line.split(APO_SEP) + if fields[0] != "apoL_a01_code": + raise ScoFormatError(f"invalid line: {line} (expecting apoL_a01_code)") + col_keys = fields while True: # skip premiere partie (apoL_a02_nom, ...) line = f.readline().strip(" " + APO_NEWLINE) @@ -1093,10 +1137,9 @@ def _apo_read_cols(f): if line == "APO_COL_VAL_FIN": break i += 1 - fs = line.split(APO_SEP) - # print fs[0], len(fs) + fields = line.split(APO_SEP) # sanity check - col_id = fs[0] # apoL_c0001, ... + col_id = fields[0] # apoL_c0001, ... if col_id in cols: raise ScoFormatError(f"duplicate column definition: {col_id}") m = re.match(r"^apoL_c([0-9]{4})$", col_id) @@ -1107,13 +1150,13 @@ def _apo_read_cols(f): if int(m.group(1)) != i: raise ScoFormatError(f"invalid column id: {col_id} for index {i}") - cols[col_id] = DictCol(list(zip(col_keys, fs))) + cols[col_id] = DictCol(list(zip(col_keys, fields))) cols[col_id].lineno = f.lineno # for debuging purpose return cols -def _apo_read_TITRES(f): +def _apo_read_TITRES(f) -> dict: "Lecture section TITRES du fichier Apogée, renvoie dict" d = {} while True: @@ -1169,10 +1212,10 @@ def nar_etuds_table(apo_data, NAR_Etuds): """Liste les NAR -> excel table""" code_etape = apo_data.etape_apogee today = datetime.datetime.today().strftime("%d/%m/%y") - L = [] + rows = [] NAR_Etuds.sort(key=lambda k: k["nom"]) for e in NAR_Etuds: - L.append( + rows.append( { "nom": e["nom"], "prenom": e["prenom"], @@ -1213,13 +1256,13 @@ def nar_etuds_table(apo_data, NAR_Etuds): "c13", "date", ) - T = GenTable( + table = GenTable( columns_ids=columns_ids, titles=dict(zip(columns_ids, columns_ids)), - rows=L, + rows=rows, xls_sheet_name="NAR ScoDoc", ) - return T.excel() + return table.excel() def export_csv_to_apogee( @@ -1304,24 +1347,28 @@ def export_csv_to_apogee( cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX logf = io.StringIO() - logf.write("export_to_apogee du %s\n\n" % time.ctime()) + logf.write(f"export_to_apogee du {time.ctime()}\n\n") logf.write("Semestres ScoDoc sources:\n") for sem in apo_data.sems_etape: logf.write("\t%(titremois)s\n" % sem) - logf.write("Periode: %s\n" % periode) - logf.write("export_res_etape: %s\n" % int(export_res_etape)) - logf.write("export_res_sem: %s\n" % int(export_res_sem)) - logf.write("export_res_ues: %s\n" % int(export_res_ues)) - logf.write("export_res_modules: %s\n" % int(export_res_modules)) - logf.write("export_res_sdj: %s\n" % int(export_res_sdj)) + + def vrai(val): + return "vrai" if int(val) else "faux" + + logf.write(f"Période: {periode}\n") + logf.write(f"exporte résultat à l'étape: {vrai(export_res_etape)}\n") + logf.write(f"exporte résultat à l'année: {vrai(export_res_sem)}\n") + logf.write(f"exporte résultats des UEs: {vrai(export_res_ues)}\n") + logf.write(f"exporte résultats des modules: {vrai(export_res_modules)}\n") + logf.write(f"exporte résultats sans décision de jury: {vrai(export_res_sdj)}\n") logf.write( - "\nEtudiants Apogee non trouves dans ScoDoc:\n" + "\nÉtudiants Apogée non trouvés dans ScoDoc:\n" + "\n".join( ["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in Apo_Non_ScoDoc] ) ) logf.write( - "\nEtudiants Apogee non inscrits sur ScoDoc dans cette étape:\n" + "\nÉtudiants Apogée non inscrits sur ScoDoc dans cette étape:\n" + "\n".join( [ "%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py index a3839fa75..dc1a50499 100644 --- a/app/scodoc/sco_etape_bilan.py +++ b/app/scodoc/sco_etape_bilan.py @@ -218,16 +218,14 @@ def help(): """ -def entete_liste_etudiant(): +def entete_liste_etudiant() -> str: return """ -

Liste des étudiants -
    -
  • Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour +
      +
    • Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour n'afficher que les étudiants correspondants
    • - - -
    -

+ + + """ @@ -488,11 +486,21 @@ class EtapeBilan: self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'" H = [ - '

Tableau des effectifs

', + """
+
+ Tableau des effectifs + + """, self._diagtable(), + """
""", self.display_tags(), + """
+ Liste des étudiants + + """, entete_liste_etudiant(), self.table_effectifs(), + """
""", help(), ] @@ -533,44 +541,38 @@ class EtapeBilan: # filtre_row: explicitation du filtre ligne éventuelle # filtre_col: explicitation du filtre colonne évnetuelle if ind_row == ROW_CUMUL and ind_col == COL_CUMUL: - javascript = "doFiltrage(%s, %s, '*', '*', '%s', '%s', '%s');" % ( - self.all_rows_str, - self.all_cols_str, - comptage, - "", - "", - ) - elif ind_row == ROW_CUMUL: - javascript = "doFiltrage(%s, %s, '*', '.%s', '%s', '%s', '%s');" % ( - self.all_rows_str, - self.all_cols_str, - ind_col, - comptage, - "", - json.dumps(self.titres[ind_col].replace("
", " / "))[1:-1], - ) - elif ind_col == COL_CUMUL: - javascript = "doFiltrage(%s, %s, '.%s', '*', '%s', '%s', '%s');" % ( - self.all_rows_str, - self.all_cols_str, - ind_row, - " (%d étudiants)" % count, - json.dumps(self.titres[ind_row])[1:-1], - "", - ) - else: - javascript = "doFiltrage(%s, %s, '.%s', '.%s', '%s', '%s', '%s');" % ( - self.all_rows_str, - self.all_cols_str, - ind_row, - ind_col, - comptage, - json.dumps(self.titres[ind_row])[1:-1], - json.dumps(self.titres[ind_col].replace("
", " / "))[1:-1], - ) - return '%d' % (javascript, count) + javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str}, + '*', '*', + '{comptage}', + '', '' + );""" - def _diagtable(self): + elif ind_row == ROW_CUMUL: + javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str}, + '*', '.{ind_col}', + '{comptage}', '', + '{json.dumps(self.titres[ind_col].replace("
", " / "))[1:-1]}' + );""" + + elif ind_col == COL_CUMUL: + javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str}, + '.{ind_row}', '*', + ' ({count} étudiants)', + '{json.dumps(self.titres[ind_row])[1:-1]}', '' + );""" + + else: + javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str}, + '.{ind_row}', '.{ind_col}', + '{comptage}', + '{json.dumps(self.titres[ind_row])[1:-1]}', + '{json.dumps(self.titres[ind_col].replace("
", " / "))[1:-1]}' + );""" + + return f"""{count}""" + + def _diagtable(self) -> str: + """Table avec les semestres et les effectifs""" H = [] liste_semestres = sorted(self.semestres.keys()) @@ -588,7 +590,7 @@ class EtapeBilan: col_ids.append(PLUSIEURS_ETAPES) self.titres["row_title"] = "Semestre" self.titres[PAS_DE_NIP] = "Hors Apogée (" + FLAG[PAS_DE_NIP] + ")" - self.titres[PAS_D_ETAPE] = "Pas d'étape (" + FLAG[PAS_D_ETAPE] + ")" + self.titres[PAS_D_ETAPE] = "Sans étape (" + FLAG[PAS_D_ETAPE] + ")" self.titres[PLUSIEURS_ETAPES] = ( "Plusieurs etapes (" + FLAG[PLUSIEURS_ETAPES] + ")" ) @@ -680,8 +682,11 @@ class EtapeBilan: NIP_NON_UNIQUE, ) H.append( - 'Code(s) nip) partagé(s) par %d étudiants
' - % (javascript, self.tag_count[NIP_NON_UNIQUE]) + f"""Code(s) nip) partagé(s) par + {self.tag_count[NIP_NON_UNIQUE]} + étudiants
+ """ ) return "\n".join(H) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index a383f5dd2..c91ee7620 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -40,6 +40,7 @@ sem_set_list() """ import flask +from flask import g, url_for from app.comp import res_sem from app.comp.res_compat import NotesTableCompat @@ -270,19 +271,41 @@ class SemSet(dict): ' (attention, plusieurs années !)' ) H.append("

") - if self["sem_id"]: - H.append( - "

Période: %(sem_id)s (1: septembre, 2: janvier)

" % self - ) + H.append( - "

Etapes: %s

" - % sco_formsemestre.etapes_apo_str(self.list_etapes()) + f"""

Période: +

+ + """ + ) + + H.append( + f"

Etapes: {sco_formsemestre.etapes_apo_str(self.list_etapes())}

" ) H.append("""

Semestres de l'ensemble:

    """) for sem in self.sems: H.append( - '
  • %(titre_num)s %(mois_debut)s - %(mois_fin)s' + """
  • %(titre_num)s %(mois_debut)s - %(mois_fin)s""" % sem ) H.append(