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, jury,
logos, logos,
partitions, partitions,
semset,
users, 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) sem_id = db.Column(db.Integer, nullable=False, default=0)
"période: 0 (année), 1 (Simpair), 2 (Spair)" "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 # Association: many to many
notes_semset_formsemestre = db.Table( notes_semset_formsemestre = db.Table(

View File

@ -764,24 +764,24 @@ class SeqGenTable(object):
# ----- Exemple d'utilisation minimal. # ----- Exemple d'utilisation minimal.
if __name__ == "__main__": if __name__ == "__main__":
T = GenTable( table = GenTable(
rows=[{"nom": "Hélène", "age": 26}, {"nom": "Titi&çà§", "age": 21}], rows=[{"nom": "Hélène", "age": 26}, {"nom": "Titi&çà§", "age": 21}],
columns_ids=("nom", "age"), columns_ids=("nom", "age"),
) )
print("--- HTML:") print("--- HTML:")
print(T.gen(format="html")) print(table.gen(format="html"))
print("\n--- XML:") print("\n--- XML:")
print(T.gen(format="xml")) print(table.gen(format="xml"))
print("\n--- JSON:") print("\n--- JSON:")
print(T.gen(format="json")) print(table.gen(format="json"))
# Test pdf: # Test pdf:
import io import io
from reportlab.platypus import KeepInFrame from reportlab.platypus import KeepInFrame
from app.scodoc import sco_preferences, sco_pdf from app.scodoc import sco_preferences, sco_pdf
preferences = sco_preferences.SemPreferences() preferences = sco_preferences.SemPreferences()
T.preferences = preferences table.preferences = preferences
objects = T.gen(format="pdf") objects = table.gen(format="pdf")
objects = [KeepInFrame(0, 0, objects, mode="shrink")] objects = [KeepInFrame(0, 0, objects, mode="shrink")]
doc = io.BytesIO() doc = io.BytesIO()
document = sco_pdf.BaseDocTemplate(doc) document = sco_pdf.BaseDocTemplate(doc)
@ -794,6 +794,6 @@ if __name__ == "__main__":
data = doc.getvalue() data = doc.getvalue()
with open("/tmp/gen_table.pdf", "wb") as f: with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data) f.write(data)
p = T.make_page(format="pdf") p = table.make_page(format="pdf")
with open("toto.pdf", "wb") as f: with open("toto.pdf", "wb") as f:
f.write(p) f.write(p)

View File

@ -140,7 +140,7 @@ def sco_header(
init_google_maps=False, # Google maps init_google_maps=False, # Google maps
init_datatables=True, init_datatables=True,
titrebandeau="", # titre dans bandeau superieur 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 user_check=True, # verifie passwords temporaires
etudid=None, etudid=None,
formsemestre_id=None, formsemestre_id=None,

View File

@ -100,7 +100,8 @@ from chardet import detect as chardet_detect
from app import log from app import log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat 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 from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@ -220,10 +221,10 @@ class ApoElt(object):
self.cols.append(col) self.cols.append(col)
def __repr__(self): 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""" """Valeurs colonnes d'un element pour un etudiant"""
def __init__(self, nip, apo_elt, init_vals): 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_sem = export_res_sem # elt_sem_apo
self.export_res_ues = export_res_ues self.export_res_ues = export_res_ues
self.export_res_modules = export_res_modules 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.export_res_rat = export_res_rat
self.fmt_note = functools.partial( self.fmt_note = functools.partial(
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f" _apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
@ -354,7 +356,8 @@ class ApoEtud(dict):
] ]
except KeyError as exc: except KeyError as exc:
log( 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( raise ScoValueError(
f"""L'élément {code} n'a pas de résultat: peut-être une erreur 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: 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 ELP élément pédagogique: UE, module
Autres éléments: résultats du semestre ou de l'année scolaire: Autres éléments: résultats du semestre ou de l'année scolaire:
=> VRTW1: code additionnel au semestre ("code élement semestre", elt_sem_apo) => 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 # pas de decision de jury, on n'enregistre rien
# (meme si démissionnaire) # (meme si démissionnaire)
if not self.has_logged_no_decision: 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 self.has_logged_no_decision = True
return VOID_APO_RES return VOID_APO_RES
@ -423,6 +426,7 @@ class ApoEtud(dict):
if export_res_etape: if export_res_etape:
return self.comp_elt_annuel(etudid, cur_sem, autre_sem) return self.comp_elt_annuel(etudid, cur_sem, autre_sem)
else: else:
self.log.append("export étape désactivé")
return VOID_APO_RES return VOID_APO_RES
# Element semestre: # Element semestre:
@ -495,43 +499,53 @@ class ApoEtud(dict):
def comp_elt_annuel(self, etudid, cur_sem, autre_sem): def comp_elt_annuel(self, etudid, cur_sem, autre_sem):
"""Calcul resultat annuel (VET) à partir du semestre courant """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) 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: # Code annuel:
# - Note: moyenne des moyennes générales des deux semestres (pas vraiment de sens, mais faute de mieux) # - Note: moyenne des moyennes générales des deux semestres
# on pourrait aussi bien prendre seulement la note du dernier semestre (S2 ou S4). Paramétrable ? # (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: # - Résultat jury:
# si l'autre est validé, code du semestre courant (ex: S1 (ADM), S2 (AJ) => année AJ) # 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 # si l'autre n'est pas validé ou est DEF ou DEM, code de l'autre
# #
# XXX cette règle est discutable, à valider # 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: if not cur_sem:
# l'étudiant n'a pas de semestre courant ?! # 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") log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
return VOID_APO_RES return VOID_APO_RES
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"]) cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre) 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: if not self.is_apc:
# ne touche pas aux RATs cur_decision = cur_nt.get_etud_decision_sem(etudid)
return VOID_APO_RES 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: if not autre_sem:
# formations monosemestre, ou code VET semestriel, # formations monosemestre, ou code VET semestriel,
# ou jury intermediaire et etudiant non redoublant... # ou jury intermediaire et etudiant non redoublant...
return self.comp_elt_semestre(cur_nt, cur_decision, etudid) 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_formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
autre_nt: NotesTableCompat = res_sem.load_formsemestre_results( autre_nt: NotesTableCompat = res_sem.load_formsemestre_results(
autre_formsemestre 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) autre_decision = autre_nt.get_etud_decision_sem(etudid)
if not autre_decision: if not autre_decision:
# pas de decision dans l'autre => pas de résultat annuel # 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="") 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): def etud_semestres_de_etape(self, apo_data):
""" """
Lorsqu'on a une formation semestrialisée mais avec un code étape annuel, 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: for sem in cur_sems:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
decision = nt.get_etud_decision_sem(self.etud["etudid"]) has_decision = nt.etud_has_decision(self.etud["etudid"])
if decision: if has_decision:
cur_sem = sem cur_sem = sem
break break
if cur_sem is None: 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: if apo_data.cur_semestre_id <= 0:
return ( return (
@ -670,7 +716,7 @@ class ApoEtud(dict):
return cur_sem, autre_sem return cur_sem, autre_sem
class ApoData(object): class ApoData:
def __init__( def __init__(
self, self,
data: str, data: str,
@ -697,8 +743,13 @@ class ApoData(object):
self.export_res_rat = export_res_rat self.export_res_rat = export_res_rat
self.orig_filename = orig_filename self.orig_filename = orig_filename
self.periode = periode # self.periode = periode #
"1 sem. sept-jan, 2 sem. fev-jul. 0 si étape en 1 seul semestre."
self.is_apc = None self.is_apc = None
"Vrai si BUT" "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: try:
self.read_csv(data) self.read_csv(data)
except ScoFormatError as e: 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) False # True si jury à mi-étape, eg jury de S1 dans l'étape (S1, S2)
) )
log( log(f"ApoData( periode={self.periode}, annee_scolaire={self.annee_scolaire} )")
"ApoData( periode=%s, annee_scolaire=%s )"
% (self.periode, self.annee_scolaire)
)
def set_periode(self, periode): # currently unused
self.periode = periode
def setup(self): def setup(self):
"""Recherche semestres ScoDoc concernés""" """Recherche semestres ScoDoc concernés"""
@ -871,16 +916,16 @@ class ApoData(object):
if not line.strip(): if not line.strip():
continue # silently ignore blank lines continue # silently ignore blank lines
line = line.strip(APO_NEWLINE) line = line.strip(APO_NEWLINE)
fs = line.split(APO_SEP) fields = line.split(APO_SEP)
cols = {} # { col_id : value } cols = {} # { col_id : value }
for i in range(len(fs)): for i, field in enumerate(fields):
cols[self.col_ids[i]] = fs[i] cols[self.col_ids[i]] = field
L.append( L.append(
ApoEtud( ApoEtud(
nip=fs[0], # id etudiant nip=fields[0], # id etudiant
nom=fs[1], nom=fields[1],
prenom=fs[2], prenom=fields[2],
naissance=fs[3], naissance=fields[3],
cols=cols, cols=cols,
export_res_etape=self.export_res_etape, export_res_etape=self.export_res_etape,
export_res_sem=self.export_res_sem, export_res_sem=self.export_res_sem,
@ -902,7 +947,8 @@ class ApoData(object):
def get_vdi_apogee(self): def get_vdi_apogee(self):
"""le VDI (version de diplôme), stocké dans l'élément VET """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(): for elt in self.apo_elts.values():
if elt.type_objet == "VET": if elt.type_objet == "VET":
@ -923,7 +969,7 @@ class ApoData(object):
m = re.match("[12][0-9]{3}", self.titles["apoC_annee"]) m = re.match("[12][0-9]{3}", self.titles["apoC_annee"])
if not m: if not m:
raise ScoFormatError( 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)) return int(m.group(0))
@ -939,10 +985,10 @@ class ApoData(object):
def write_etuds(self, f): def write_etuds(self, f):
"""write apo CSV etuds on f""" """write apo CSV etuds on f"""
for e in self.etuds: 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: for col_id in self.col_ids:
try: try:
fs.append(str(e.new_cols[col_id])) fields.append(str(e.new_cols[col_id]))
except KeyError: except KeyError:
log( log(
"Error: %s %s missing column key %s" "Error: %s %s missing column key %s"
@ -952,19 +998,18 @@ class ApoData(object):
log("col_ids=%s" % pprint.pformat(self.col_ids)) log("col_ids=%s" % pprint.pformat(self.col_ids))
log("etudiant ignore.\n") 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 """Liste des codes des elements Apogee non trouvés dans ScoDoc
(après traitement de tous les étudiants) (après traitement de tous les étudiants)
""" """
s = set() codes = set()
for e in self.etuds: for e in self.etuds:
ul = [code for code in e.col_elts if e.col_elts[code] is None] codes.update({code for code in e.col_elts if e.col_elts[code] is None})
s.update(ul) codes_list = list(codes)
L = list(s) codes_list.sort()
L.sort() return codes_list
return L
def list_elements(self): def list_elements(self):
"""Liste les codes des elements Apogée de la maquette """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 declared = self.col_ids[4:] # id des colones dans l'en-tête
present = sorted(self.cols.keys()) # colones presentes present = sorted(self.cols.keys()) # colones presentes
log("Fichier Apogee invalide:") log("Fichier Apogee invalide:")
log("Colonnes declarees: %s" % declared) log(f"Colonnes declarees: {declared}")
log("Colonnes presentes: %s" % present) log(f"Colonnes presentes: {present}")
raise ScoFormatError( raise ScoFormatError(
"""Fichier Apogee invalide<br>Colonnes declarees: <tt>%s</tt> f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
<br>Colonnes presentes: <tt>%s</tt>""" <br>Colonnes presentes: <tt>{present}</tt>"""
% (declared, present)
) )
# l'ensemble de tous les codes des elements apo des semestres: # l'ensemble de tous les codes des elements apo des semestres:
sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set()) sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set())
return maq_elems, sem_elems 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 """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). qui s'y trouvent (dans le semestre, les UE ou les modules).
Return: { formsemestre_id : { 'code1', 'code2', ... }} Return: { formsemestre_id : { 'code1', 'code2', ... }}
@ -1040,7 +1084,7 @@ class ApoData(object):
"est_NAR": e.is_NAR, "est_NAR": e.is_NAR,
"commentaire": "; ".join(e.log), "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"] = e.col_elts[self.etape_apogee].get("R", "")
cr["etape_note"] = e.col_elts[self.etape_apogee].get("N", "") cr["etape_note"] = e.col_elts[self.etape_apogee].get("N", "")
else: else:
@ -1073,10 +1117,10 @@ def _apo_read_cols(f):
Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... } Example: { 'apoL_c0001' : { 'Type Objet' : 'VET', 'Code' : 'V1IN', ... }, ... }
""" """
line = f.readline().strip(" " + APO_NEWLINE) line = f.readline().strip(" " + APO_NEWLINE)
fs = line.split(APO_SEP) fields = line.split(APO_SEP)
if fs[0] != "apoL_a01_code": if fields[0] != "apoL_a01_code":
raise ScoFormatError("invalid line: %s (expecting apoL_a01_code)" % line) raise ScoFormatError(f"invalid line: {line} (expecting apoL_a01_code)")
col_keys = fs col_keys = fields
while True: # skip premiere partie (apoL_a02_nom, ...) while True: # skip premiere partie (apoL_a02_nom, ...)
line = f.readline().strip(" " + APO_NEWLINE) line = f.readline().strip(" " + APO_NEWLINE)
@ -1090,10 +1134,9 @@ def _apo_read_cols(f):
if line == "APO_COL_VAL_FIN": if line == "APO_COL_VAL_FIN":
break break
i += 1 i += 1
fs = line.split(APO_SEP) fields = line.split(APO_SEP)
# print fs[0], len(fs)
# sanity check # sanity check
col_id = fs[0] # apoL_c0001, ... col_id = fields[0] # apoL_c0001, ...
if col_id in cols: if col_id in cols:
raise ScoFormatError(f"duplicate column definition: {col_id}") raise ScoFormatError(f"duplicate column definition: {col_id}")
m = re.match(r"^apoL_c([0-9]{4})$", 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: if int(m.group(1)) != i:
raise ScoFormatError(f"invalid column id: {col_id} for index {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 cols[col_id].lineno = f.lineno # for debuging purpose
return cols return cols
def _apo_read_TITRES(f): def _apo_read_TITRES(f) -> dict:
"Lecture section TITRES du fichier Apogée, renvoie dict" "Lecture section TITRES du fichier Apogée, renvoie dict"
d = {} d = {}
while True: while True:
@ -1166,10 +1209,10 @@ def nar_etuds_table(apo_data, NAR_Etuds):
"""Liste les NAR -> excel table""" """Liste les NAR -> excel table"""
code_etape = apo_data.etape_apogee code_etape = apo_data.etape_apogee
today = datetime.datetime.today().strftime("%d/%m/%y") today = datetime.datetime.today().strftime("%d/%m/%y")
L = [] rows = []
NAR_Etuds.sort(key=lambda k: k["nom"]) NAR_Etuds.sort(key=lambda k: k["nom"])
for e in NAR_Etuds: for e in NAR_Etuds:
L.append( rows.append(
{ {
"nom": e["nom"], "nom": e["nom"],
"prenom": e["prenom"], "prenom": e["prenom"],
@ -1210,13 +1253,13 @@ def nar_etuds_table(apo_data, NAR_Etuds):
"c13", "c13",
"date", "date",
) )
T = GenTable( table = GenTable(
columns_ids=columns_ids, columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)), titles=dict(zip(columns_ids, columns_ids)),
rows=L, rows=rows,
xls_sheet_name="NAR ScoDoc", xls_sheet_name="NAR ScoDoc",
) )
return T.excel() return table.excel()
def export_csv_to_apogee( def export_csv_to_apogee(
@ -1301,24 +1344,28 @@ def export_csv_to_apogee(
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
logf = io.StringIO() 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") logf.write("Semestres ScoDoc sources:\n")
for sem in apo_data.sems_etape: for sem in apo_data.sems_etape:
logf.write("\t%(titremois)s\n" % sem) logf.write("\t%(titremois)s\n" % sem)
logf.write("Periode: %s\n" % periode)
logf.write("export_res_etape: %s\n" % int(export_res_etape)) def vrai(val):
logf.write("export_res_sem: %s\n" % int(export_res_sem)) return "vrai" if int(val) else "faux"
logf.write("export_res_ues: %s\n" % int(export_res_ues))
logf.write("export_res_modules: %s\n" % int(export_res_modules)) logf.write(f"Période: {periode}\n")
logf.write("export_res_sdj: %s\n" % int(export_res_sdj)) 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( logf.write(
"\nEtudiants Apogee non trouves dans ScoDoc:\n" "\nÉtudiants Apogée non trouvés dans ScoDoc:\n"
+ "\n".join( + "\n".join(
["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in Apo_Non_ScoDoc] ["%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) for e in Apo_Non_ScoDoc]
) )
) )
logf.write( 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( + "\n".join(
[ [
"%s\t%s\t%s" % (e["nip"], e["nom"], e["prenom"]) "%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 formsemestre, res, include_evaluations=True
) )
if table_html: if table_html:
flash(f"Moyennes archivées le {date}", category="info")
data = "\n".join( data = "\n".join(
[ [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=f"Moyennes archivées le {date}", page_title=f"Moyennes archivées le {date}",
head_message=f"Moyennes archivées le {date}",
no_side_bar=True, no_side_bar=True,
), ),
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>', 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. les dossiers d'admission et autres pièces utiles.
""" """
import flask import flask
from flask import url_for, render_template from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
from flask_login import current_user 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_import_etuds
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_trombino from app.scodoc import sco_trombino
from app.scodoc import sco_excel
from app.scodoc import sco_archives from app.scodoc import sco_archives
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError 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) EtudsArchive.delete_archive(archive_id)
flash("Archive supprimée")
return flask.redirect( return flask.redirect(
url_for( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=etudid,
head_message="Archive%20supprimée",
)
) )

View File

@ -32,7 +32,7 @@ import io
from zipfile import ZipFile from zipfile import ZipFile
import flask 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 import app.scodoc.sco_utils as scu
from app import log from app import log
@ -692,8 +692,10 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
) )
if info: if info:
sco_etape_apogee.apo_csv_delete(info["archive_id"]) sco_etape_apogee.apo_csv_delete(info["archive_id"])
return flask.redirect(dest_url + "&head_message=Archive%20supprimée") flash("Archive supprimée")
return flask.redirect(dest_url + "&head_message=Archive%20inexistante") 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"): def view_apo_csv(etape_apo="", semset_id="", format="html"):

View File

@ -218,16 +218,14 @@ def help():
</div> """ </div> """
def entete_liste_etudiant(): def entete_liste_etudiant() -> str:
return """ return """
<h4 id='effectifs'>Liste des étudiants <span id='compte'></span> <ul>
<ul> <li id="sans_filtre">Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour
<li id='sans_filtre'>Pas de filtrage: Cliquez sur un des nombres du tableau ci-dessus pour
n'afficher que les étudiants correspondants</li> n'afficher que les étudiants correspondants</li>
<li id='filtre_row' style='display:none'></li> <li id="filtre_row" style="display:none"></li>
<li id='filtre_col' style='display:none'></li> <li id="filtre_col" style="display:none"></li>
</ul> </ul>
</h4>
""" """
@ -488,11 +486,21 @@ class EtapeBilan:
self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'" self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
H = [ 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(), self._diagtable(),
"""</details>""",
self.display_tags(), self.display_tags(),
"""<details open="true">
<summary><b id="effectifs">Liste des étudiants <span id="compte"></span></b>
</summary>
""",
entete_liste_etudiant(), entete_liste_etudiant(),
self.table_effectifs(), self.table_effectifs(),
"""</details>""",
help(), help(),
] ]
@ -533,44 +541,38 @@ class EtapeBilan:
# filtre_row: explicitation du filtre ligne éventuelle # filtre_row: explicitation du filtre ligne éventuelle
# filtre_col: explicitation du filtre colonne évnetuelle # filtre_col: explicitation du filtre colonne évnetuelle
if ind_row == ROW_CUMUL and ind_col == COL_CUMUL: if ind_row == ROW_CUMUL and ind_col == COL_CUMUL:
javascript = "doFiltrage(%s, %s, '*', '*', '%s', '%s', '%s');" % ( javascript = f"""doFiltrage({self.all_rows_str}, {self.all_cols_str},
self.all_rows_str, '*', '*',
self.all_cols_str, '{comptage}',
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)
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 = [] H = []
liste_semestres = sorted(self.semestres.keys()) liste_semestres = sorted(self.semestres.keys())
@ -588,7 +590,7 @@ class EtapeBilan:
col_ids.append(PLUSIEURS_ETAPES) col_ids.append(PLUSIEURS_ETAPES)
self.titres["row_title"] = "Semestre" self.titres["row_title"] = "Semestre"
self.titres[PAS_DE_NIP] = "Hors Apogée (" + FLAG[PAS_DE_NIP] + ")" 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] = ( self.titres[PLUSIEURS_ETAPES] = (
"Plusieurs etapes (" + FLAG[PLUSIEURS_ETAPES] + ")" "Plusieurs etapes (" + FLAG[PLUSIEURS_ETAPES] + ")"
) )
@ -680,8 +682,11 @@ class EtapeBilan:
NIP_NON_UNIQUE, NIP_NON_UNIQUE,
) )
H.append( H.append(
'Code(s) nip) partagé(s) par <a href="#synthèse" onclick="%s">%d</a> étudiants<br>' f"""Code(s) nip) partagé(s) par
% (javascript, self.tag_count[NIP_NON_UNIQUE]) <a href="#synthèse" class="stdlink"
onclick="{javascript}">{self.tag_count[NIP_NON_UNIQUE]}</a>
étudiants<br>
"""
) )
return "\n".join(H) return "\n".join(H)

View File

@ -1179,9 +1179,13 @@ def formsemestre_clone(formsemestre_id):
clone_evaluations=tf[2]["clone_evaluations"], clone_evaluations=tf[2]["clone_evaluations"],
clone_partitions=tf[2]["clone_partitions"], clone_partitions=tf[2]["clone_partitions"],
) )
flash("Nouveau semestre créé")
return flask.redirect( return flask.redirect(
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé" url_for(
% new_formsemestre_id "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... # Bon, s'il le faut...
do_formsemestre_delete(formsemestre_id) 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): def formsemestre_has_decisions_or_compensations(formsemestre: FormSemestre):

View File

@ -111,7 +111,7 @@ get_base_preferences(formsemestre_id)
""" """
import flask import flask
from flask import current_app, g, request, url_for from flask import flash, g, request
# from flask_login import current_user # from flask_login import current_user
@ -2127,7 +2127,8 @@ class BasePreferences(object):
for pref in self.prefs_definition: for pref in self.prefs_definition:
self.prefs[None][pref[0]] = tf[2][pref[0]] self.prefs[None][pref[0]] = tf[2][pref[0]]
self.save() 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): def build_tf_form(self, categories: list[str] = None, formsemestre_id: int = None):
"""Build list of elements for TrivialFormulator. """Build list of elements for TrivialFormulator.
@ -2299,7 +2300,8 @@ function set_global_pref(el, pref_name) {
if tf[0] == 0: if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect(dest_url + "&head_message=Annulé") # cancel flash("Annulé")
return flask.redirect(dest_url)
else: else:
# Supprime pref locale du semestre (retour à la valeur globale) # Supprime pref locale du semestre (retour à la valeur globale)
if tf[2]["suppress"]: if tf[2]["suppress"]:
@ -2333,7 +2335,8 @@ function set_global_pref(el, pref_name) {
# done: change prefs and redirect to semestre status # done: change prefs and redirect to semestre status
destination = tf[2]["destination"] destination = tf[2]["destination"]
if destination == "done" or 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": elif destination == "again":
return flask.redirect( return flask.redirect(
request.base_url + "?formsemestre_id=" + str(self.formsemestre_id) request.base_url + "?formsemestre_id=" + str(self.formsemestre_id)

View File

@ -40,6 +40,7 @@ sem_set_list()
""" """
import flask import flask
from flask import g, url_for
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
@ -270,19 +271,41 @@ class SemSet(dict):
' <span class="redboldtext">(attention, plusieurs années !)</span>' ' <span class="redboldtext">(attention, plusieurs années !)</span>'
) )
H.append("</p>") 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( H.append(
"<p>Etapes: <tt>%s</tt></p>" f"""<p>Période: <select name="periode" onchange="set_periode(this);">
% sco_formsemestre.etapes_apo_str(self.list_etapes()) <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">""") H.append("""<h4>Semestres de l'ensemble:</h4><ul class="semset_listsems">""")
for sem in self.sems: for sem in self.sems:
H.append( 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 % sem
) )
H.append( H.append(

View File

@ -1177,12 +1177,12 @@ def edit_moduleimpl_expr(moduleimpl_id):
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=sem["formsemestre_id"] formsemestre_id=sem["formsemestre_id"]
) # > modif regle calcul ) # > modif regle calcul
flash("règle de calcul modifiée")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id, 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", "notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
head_message="formule supprimée",
) )
) )
@ -1993,7 +1992,7 @@ def formsemestre_bulletins_choice(
""", """,
] ]
H.append("""<select name="version" class="noprint">""") H.append("""<select name="version" class="noprint">""")
for (version, description) in ( for version, description in (
("short", "Version courte"), ("short", "Version courte"),
("selectedevals", "Version intermédiaire"), ("selectedevals", "Version intermédiaire"),
("long", "Version complète"), ("long", "Version complète"),

View File

@ -732,12 +732,12 @@ def doSuppressAnnotation(etudid, annotation_id):
logdb(cnx, method="SuppressAnnotation", etudid=etudid) logdb(cnx, method="SuppressAnnotation", etudid=etudid)
sco_etud.etud_annotations_delete(cnx, annotation_id) sco_etud.etud_annotations_delete(cnx, annotation_id)
flash("Annotation supprimée")
return flask.redirect( return flask.redirect(
url_for( url_for(
"scolar.ficheEtud", "scolar.ficheEtud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etudid, 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"]] to_inval = [s["formsemestre_id"] for s in etud["sems"]]
for formsemestre_id in to_inval: for formsemestre_id in to_inval:
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id) # > 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") @bp.route("/check_group_apogee")