Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into table

This commit is contained in:
Emmanuel Viennet 2023-04-17 04:07:27 +02:00
commit 47ed37e90e
15 changed files with 296 additions and 167 deletions

View File

@ -46,5 +46,6 @@ from app.api import (
jury,
logos,
partitions,
semset,
users,
)

39
app/api/semset.py Normal file
View File

@ -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/<int:semset_id>", methods=["POST"])
@api_web_bp.route("/semset/set_periode/<int:semset_id>", 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})

View File

@ -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(

View File

@ -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)

View File

@ -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,

View File

@ -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<br>Colonnes declarees: <tt>%s</tt>
<br>Colonnes presentes: <tt>%s</tt>"""
% (declared, present)
f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
<br>Colonnes presentes: <tt>{present}</tt>"""
)
# 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"])

View File

@ -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'<h2 class="fontorange">Valeurs archivées le {date}</h2>',

View File

@ -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)
)

View File

@ -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"):

View File

@ -218,16 +218,14 @@ def help():
</div> """
def entete_liste_etudiant():
def entete_liste_etudiant() -> str:
return """
<h4 id='effectifs'>Liste des étudiants <span id='compte'></span>
<ul>
<li id='sans_filtre'>Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour
<ul>
<li id="sans_filtre">Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour
n'afficher que les étudiants correspondants</li>
<li id='filtre_row' style='display:none'></li>
<li id='filtre_col' style='display:none'></li>
</ul>
</h4>
<li id="filtre_row" style="display:none"></li>
<li id="filtre_col" style="display:none"></li>
</ul>
"""
@ -488,11 +486,21 @@ class EtapeBilan:
self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
H = [
'<div id="synthese" class=u"semset_description"><h4>Tableau des effectifs</h4>',
"""<div id="synthese" class="semset_description">
<details open="true">
<summary><b>Tableau des effectifs</b>
</summary>
""",
self._diagtable(),
"""</details>""",
self.display_tags(),
"""<details open="true">
<summary><b id="effectifs">Liste des étudiants <span id="compte"></span></b>
</summary>
""",
entete_liste_etudiant(),
self.table_effectifs(),
"""</details>""",
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("<br>", " / "))[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("<br>", " / "))[1:-1],
)
return '<a href="#synthese" onclick="%s">%d</a>' % (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("<br>", " / "))[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("<br>", " / "))[1:-1]}'
);"""
return f"""<a href="#synthese" class="stdlink" onclick="{javascript}">{count}</a>"""
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 <a href="#synthèse" onclick="%s">%d</a> étudiants<br>'
% (javascript, self.tag_count[NIP_NON_UNIQUE])
f"""Code(s) nip) partagé(s) par
<a href="#synthèse" class="stdlink"
onclick="{javascript}">{self.tag_count[NIP_NON_UNIQUE]}</a>
étudiants<br>
"""
)
return "\n".join(H)

View File

@ -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):

View File

@ -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)

View File

@ -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):
' <span class="redboldtext">(attention, plusieurs années !)</span>'
)
H.append("</p>")
if self["sem_id"]:
H.append(
"<p>Période: %(sem_id)s (<em>1: septembre, 2: janvier</em>)</p>" % self
)
H.append(
"<p>Etapes: <tt>%s</tt></p>"
% sco_formsemestre.etapes_apo_str(self.list_etapes())
f"""<p>Période: <select name="periode" onchange="set_periode(this);">
<option value="1" {"selected" if self["sem_id"] == 1 else ""}>1re période (S1, S3)</option>
<option value="2" {"selected" if self["sem_id"] == 2 else ""}>2de période (S2, S4)</option>
<option value="0" {"selected" if self["sem_id"] == 0 else ""}>non semestrialisée (LP, ...)</option>
</select>
</p>
<script>
function set_periode(elt) {{
fetch(
"{ url_for("apiweb.semset_set_periode", scodoc_dept=g.scodoc_dept,
semset_id=self.semset_id )
}",
{{
method: "POST",
headers: {{
'Content-Type': 'application/json'
}},
body: JSON.stringify( elt.value )
}},
).then(sco_message("période modifiée"));
}};
</script>
"""
)
H.append(
f"<p>Etapes: <tt>{sco_formsemestre.etapes_apo_str(self.list_etapes())}</tt></p>"
)
H.append("""<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">""")
for sem in self.sems:
H.append(
'<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a> %(mois_debut)s - %(mois_fin)s'
"""<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s</a> %(mois_debut)s - %(mois_fin)s"""
% sem
)
H.append(

View File

@ -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("""<select name="version" class="noprint">""")
for (version, description) in (
for version, description in (
("short", "Version courte"),
("selectedevals", "Version intermédiaire"),
("long", "Version complète"),

View File

@ -732,12 +732,12 @@ def doSuppressAnnotation(etudid, annotation_id):
logdb(cnx, method="SuppressAnnotation", etudid=etudid)
sco_etud.etud_annotations_delete(cnx, annotation_id)
flash("Annotation supprimée")
return flask.redirect(
url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="Annotation%%20supprimée",
)
)
@ -1804,7 +1804,8 @@ def etudident_delete(etudid, dialog_confirmed=False):
to_inval = [s["formsemestre_id"] for s in etud["sems"]]
for formsemestre_id in to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) # >
return flask.redirect(scu.ScoURL() + r"?head_message=Etudiant%20supprimé")
flash("Étudiant supprimé !")
return flask.redirect(scu.ScoURL())
@bp.route("/check_group_apogee")