Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into table
This commit is contained in:
commit
47ed37e90e
@ -46,5 +46,6 @@ from app.api import (
|
||||
jury,
|
||||
logos,
|
||||
partitions,
|
||||
semset,
|
||||
users,
|
||||
)
|
||||
|
39
app/api/semset.py
Normal file
39
app/api/semset.py
Normal 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})
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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"])
|
||||
|
@ -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>',
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
@ -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"):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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"),
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user