diff --git a/app/api/__init__.py b/app/api/__init__.py index 90744703..d5b43688 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 00000000..f1239534 --- /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 d256cf7a..6e00649d 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -1094,6 +1094,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 6b2dcb78..436ecde2 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -764,24 +764,24 @@ class SeqGenTable(object): # ----- 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) @@ -794,6 +794,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/html_sco_header.py b/app/scodoc/html_sco_header.py index 9f537be8..76e8727a 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -140,7 +140,7 @@ def sco_header( init_google_maps=False, # Google maps init_datatables=True, titrebandeau="", # titre dans bandeau superieur - head_message="", # message action (petit cadre jaune en haut) + head_message="", # message action (petit cadre jaune en haut) DEPRECATED user_check=True, # verifie passwords temporaires etudid=None, formsemestre_id=None, diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 2cfb1cc5..928d4b4a 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', ... }} @@ -1040,7 +1084,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: @@ -1073,10 +1117,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) @@ -1090,10 +1134,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) @@ -1104,13 +1147,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: @@ -1166,10 +1209,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"], @@ -1210,13 +1253,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( @@ -1301,24 +1344,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_archives.py b/app/scodoc/sco_archives.py index 763ecb0b..17e2fe2a 100644 --- a/app/scodoc/sco_archives.py +++ b/app/scodoc/sco_archives.py @@ -342,11 +342,11 @@ def do_formsemestre_archive( formsemestre, res, include_evaluations=True ) if table_html: + flash(f"Moyennes archivées le {date}", category="info") data = "\n".join( [ html_sco_header.sco_header( page_title=f"Moyennes archivées le {date}", - head_message=f"Moyennes archivées le {date}", no_side_bar=True, ), f'

Valeurs archivées le {date}

', diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index 27189214..1ca8eece 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -30,7 +30,7 @@ les dossiers d'admission et autres pièces utiles. """ import flask -from flask import url_for, render_template +from flask import flash, render_template, url_for from flask import g, request from flask_login import current_user @@ -38,7 +38,6 @@ import app.scodoc.sco_utils as scu from app.scodoc import sco_import_etuds from app.scodoc import sco_groups from app.scodoc import sco_trombino -from app.scodoc import sco_excel from app.scodoc import sco_archives from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import AccessDenied, ScoValueError @@ -233,13 +232,9 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False): ) EtudsArchive.delete_archive(archive_id) + flash("Archive supprimée") return flask.redirect( - url_for( - "scolar.ficheEtud", - scodoc_dept=g.scodoc_dept, - etudid=etudid, - head_message="Archive%20supprimée", - ) + url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) ) diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py index 35be5a66..b53fe5e0 100644 --- a/app/scodoc/sco_etape_apogee_view.py +++ b/app/scodoc/sco_etape_apogee_view.py @@ -32,7 +32,7 @@ import io from zipfile import ZipFile import flask -from flask import url_for, g, send_file, request +from flask import flash, g, request, send_file, url_for import app.scodoc.sco_utils as scu from app import log @@ -692,8 +692,10 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False): ) if info: sco_etape_apogee.apo_csv_delete(info["archive_id"]) - return flask.redirect(dest_url + "&head_message=Archive%20supprimée") - return flask.redirect(dest_url + "&head_message=Archive%20inexistante") + flash("Archive supprimée") + return flask.redirect(dest_url) + flash("Archive inexistante", category="error") + return flask.redirect(dest_url) def view_apo_csv(etape_apo="", semset_id="", format="html"): diff --git a/app/scodoc/sco_etape_bilan.py b/app/scodoc/sco_etape_bilan.py index a3839fa7..dc1a5049 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_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 999fd525..d64bf4d9 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -1179,9 +1179,13 @@ def formsemestre_clone(formsemestre_id): clone_evaluations=tf[2]["clone_evaluations"], clone_partitions=tf[2]["clone_partitions"], ) + flash("Nouveau semestre créé") return flask.redirect( - "formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé" - % new_formsemestre_id + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=new_formsemestre_id, + ) ) @@ -1364,7 +1368,8 @@ def formsemestre_delete2(formsemestre_id, dialog_confirmed=False): ) # Bon, s'il le faut... do_formsemestre_delete(formsemestre_id) - return flask.redirect(scu.ScoURL() + "?head_message=Semestre%20supprimé") + flash("Semestre supprimé !") + return flask.redirect(scu.ScoURL()) def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre): diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index a337aa16..6340d6e8 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -111,7 +111,7 @@ get_base_preferences(formsemestre_id) """ import flask -from flask import current_app, g, request, url_for +from flask import flash, g, request # from flask_login import current_user @@ -2127,7 +2127,8 @@ class BasePreferences(object): for pref in self.prefs_definition: self.prefs[None][pref[0]] = tf[2][pref[0]] self.save() - return flask.redirect(scu.ScoURL() + "?head_message=Préférences modifiées") + flash("Préférences modifiées") + return flask.redirect(scu.ScoURL()) def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None): """Build list of elements for TrivialFormulator. @@ -2299,7 +2300,8 @@ function set_global_pref(el, pref_name) { if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() elif tf[0] == -1: - return flask.redirect(dest_url + "&head_message=Annulé") # cancel + flash("Annulé") + return flask.redirect(dest_url) else: # Supprime pref locale du semestre (retour à la valeur globale) if tf[2]["suppress"]: @@ -2333,7 +2335,8 @@ function set_global_pref(el, pref_name) { # done: change prefs and redirect to semestre status destination = tf[2]["destination"] if destination == "done" or destination == "": - return flask.redirect(dest_url + "&head_message=Préférences modifiées") + flash("Préférences modifiées") + return flask.redirect(dest_url) elif destination == "again": return flask.redirect( request.base_url + "?formsemestre_id=" + str(self.formsemestre_id) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index a383f5dd..c91ee762 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( diff --git a/app/views/notes.py b/app/views/notes.py index 09f3dee6..d8515925 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1177,12 +1177,12 @@ def edit_moduleimpl_expr(moduleimpl_id): sco_cache.invalidate_formsemestre( formsemestre_id=sem["formsemestre_id"] ) # > modif regle calcul + flash("règle de calcul modifiée") return flask.redirect( url_for( "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id, - head_message="règle%20de%20calcul%20modifiée", ) ) @@ -1305,7 +1305,6 @@ def delete_ue_expr(formsemestre_id: int, ue_id: int): "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, - head_message="formule supprimée", ) ) @@ -1993,7 +1992,7 @@ def formsemestre_bulletins_choice( """, ] H.append("""