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

This commit is contained in:
ScoDoc service 2023-05-12 21:30:22 +02:00
commit 793dfc4353
11 changed files with 104 additions and 71 deletions

View File

@ -30,7 +30,7 @@ def after_cas_login():
flask.session.get("CAS_USERNAME"), flask.session.get("CAS_USERNAME"),
) )
if cas_id is not None: if cas_id is not None:
user: User = User.query.filter_by(cas_id=cas_id).first() user: User = User.query.filter_by(cas_id=str(cas_id)).first()
if user and user.active: if user and user.active:
if user.cas_allow_login: if user.cas_allow_login:
current_app.logger.info(f"CAS: login {user.user_name}") current_app.logger.info(f"CAS: login {user.user_name}")

View File

@ -285,9 +285,9 @@ class BulletinBUT:
eval_notes[etud.id], eval_notes[etud.id],
note_max=e.note_max, note_max=e.note_max,
), ),
"min": fmt_note(notes_ok.min()), "min": fmt_note(notes_ok.min(), note_max=e.note_max),
"max": fmt_note(notes_ok.max()), "max": fmt_note(notes_ok.max(), note_max=e.note_max),
"moy": fmt_note(notes_ok.mean()), "moy": fmt_note(notes_ok.mean(), note_max=e.note_max),
}, },
"poids": poids, "poids": poids,
"url": url_for( "url": url_for(

View File

@ -78,6 +78,7 @@ from chardet import detect as chardet_detect
from app import log from app import log
from app.scodoc.sco_exceptions import ScoFormatError from app.scodoc.sco_exceptions import ScoFormatError
from app.scodoc import sco_preferences
APO_PORTAL_ENCODING = ( APO_PORTAL_ENCODING = (
"utf8" # encodage du fichier CSV Apogée (était 'ISO-8859-1' avant jul. 2016) "utf8" # encodage du fichier CSV Apogée (était 'ISO-8859-1' avant jul. 2016)
@ -384,9 +385,12 @@ col_ids={pprint.pformat(self.col_ids)}
"""write apo CSV header on f """write apo CSV header on f
(beginning of CSV until columns titles just after XX-APO_VALEURS-XX line) (beginning of CSV until columns titles just after XX-APO_VALEURS-XX line)
""" """
remove_typ_res = sco_preferences.get_preference("export_res_remove_typ_res")
for section, data in self.sections_str.items(): for section, data in self.sections_str.items():
if section != "XX-APO_VALEURS-XX": # ne recopie pas la section résultats, et en option supprime APO_TYP_RES
# XXX TODO ici on va filtrer XX-APO_TYP_RES-XX if (section != "XX-APO_VALEURS-XX") and (
section != "XX-APO_TYP_RES-XX" or not remove_typ_res
):
f.write(data) f.write(data)
f.write("XX-APO_VALEURS-XX" + APO_NEWLINE) f.write("XX-APO_VALEURS-XX" + APO_NEWLINE)

View File

@ -144,7 +144,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
""" """
from app.scodoc import sco_abs from app.scodoc import sco_abs
if not version in scu.BULLETINS_VERSIONS: if version not in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !") raise ValueError("invalid version code !")
prefs = sco_preferences.SemPreferences(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id)

View File

@ -388,10 +388,10 @@ def _list_modimpls(
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]: if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
if prefs["bul_show_minmax_eval"]: if prefs["bul_show_minmax_eval"]:
eval_dict["min"] = scu.fmt_note(etat["mini"]) eval_dict["min"] = etat["mini"] # chaine, sur 20
eval_dict["max"] = scu.fmt_note(etat["maxi"]) eval_dict["max"] = etat["maxi"]
if prefs["bul_show_moypromo"]: if prefs["bul_show_moypromo"]:
eval_dict["moy"] = scu.fmt_note(etat["moy"]) eval_dict["moy"] = etat["moy"]
mod_dict["evaluation"].append(eval_dict) mod_dict["evaluation"].append(eval_dict)

View File

@ -69,6 +69,7 @@ from app.scodoc import sco_groups
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import gen_tables from app.scodoc import gen_tables
# Important: Le nom de la classe ne doit pas changer (bien le choisir), # Important: Le nom de la classe ne doit pas changer (bien le choisir),
# car il sera stocké en base de données (dans les préférences) # car il sera stocké en base de données (dans les préférences)
class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
@ -685,10 +686,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]: if prefs["bul_show_minmax_eval"] or prefs["bul_show_moypromo"]:
etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) etat = sco_evaluations.do_evaluation_etat(e["evaluation_id"])
if prefs["bul_show_minmax_eval"]: if prefs["bul_show_minmax_eval"]:
t["min"] = scu.fmt_note(etat["mini"]) t["min"] = etat["mini"]
t["max"] = scu.fmt_note(etat["maxi"]) t["max"] = etat["maxi"]
if prefs["bul_show_moypromo"]: if prefs["bul_show_moypromo"]:
t["moy"] = scu.fmt_note(etat["moy"]) t["moy"] = etat["moy"]
P.append(t) P.append(t)
nbeval += 1 nbeval += 1
return nbeval return nbeval

View File

@ -97,11 +97,22 @@ def ListMedian(L):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False): def do_evaluation_etat(
"""donne infos sur l'état de l'évaluation evaluation_id: int, partition_id: int = None, select_first_partition=False
{ nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att, ) -> dict:
moyenne, mediane, mini, maxi, """Donne infos sur l'état de l'évaluation.
date_last_modif, gr_complets, gr_incomplets, evalcomplete } Ancienne fonction, lente: préférer ModuleImplResults pour tout calcul.
{
nb_inscrits : inscrits au module
nb_notes
nb_abs,
nb_neutre,
nb_att,
moy, median, mini, maxi : # notes, en chaine, sur 20
last_modif: datetime,
gr_complets, gr_incomplets,
evalcomplete
}
evalcomplete est vrai si l'eval est complete (tous les inscrits evalcomplete est vrai si l'eval est complete (tous les inscrits
à ce module ont des notes) à ce module ont des notes)
evalattente est vrai s'il ne manque que des notes en attente evalattente est vrai s'il ne manque que des notes en attente
@ -137,7 +148,7 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
insmod = sco_moduleimpl.do_moduleimpl_inscription_list( insmod = sco_moduleimpl.do_moduleimpl_inscription_list(
moduleimpl_id=E["moduleimpl_id"] moduleimpl_id=E["moduleimpl_id"]
) )
insmodset = set([x["etudid"] for x in insmod]) insmodset = {x["etudid"] for x in insmod}
# retire de insem ceux qui ne sont pas inscrits au module # retire de insem ceux qui ne sont pas inscrits au module
ins = [i for i in insem if i["etudid"] in insmodset] ins = [i for i in insem if i["etudid"] in insmodset]
@ -155,14 +166,13 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes) moy_num, median_num, mini_num, maxi_num = notes_moyenne_median_mini_maxi(notes)
if moy_num is None: if moy_num is None:
median, moy = "", "" median, moy = "", ""
median_num, moy_num = None, None
mini, maxi = "", "" mini, maxi = "", ""
mini_num, maxi_num = None, None maxi_num = None
else: else:
median = scu.fmt_note(median_num) median = scu.fmt_note(median_num)
moy = scu.fmt_note(moy_num) moy = scu.fmt_note(moy_num, E["note_max"])
mini = scu.fmt_note(mini_num) mini = scu.fmt_note(mini_num, E["note_max"])
maxi = scu.fmt_note(maxi_num) maxi = scu.fmt_note(maxi_num, E["note_max"])
# cherche date derniere modif note # cherche date derniere modif note
if len(etuds_notes_dict): if len(etuds_notes_dict):
t = [x["date"] for x in etuds_notes_dict.values()] t = [x["date"] for x in etuds_notes_dict.values()]
@ -226,28 +236,22 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
# Calcul moyenne dans chaque groupe de TD # Calcul moyenne dans chaque groupe de TD
gr_moyennes = [] # group : {moy,median, nb_notes} gr_moyennes = [] # group : {moy,median, nb_notes}
for group_id in GrNotes.keys(): for group_id, notes in GrNotes.items():
notes = GrNotes[group_id]
gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes) gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
gr_moyennes.append( gr_moyennes.append(
{ {
"group_id": group_id, "group_id": group_id,
"group_name": groups[group_id]["group_name"], "group_name": groups[group_id]["group_name"],
"gr_moy_num": gr_moy, "gr_moy": scu.fmt_note(gr_moy, E["note_max"]),
"gr_moy": scu.fmt_note(gr_moy), "gr_median": scu.fmt_note(gr_median, E["note_max"]),
"gr_median_num": gr_median, "gr_mini": scu.fmt_note(gr_mini, E["note_max"]),
"gr_median": scu.fmt_note(gr_median), "gr_maxi": scu.fmt_note(gr_maxi, E["note_max"]),
"gr_mini": scu.fmt_note(gr_mini),
"gr_maxi": scu.fmt_note(gr_maxi),
"gr_mini_num": gr_mini,
"gr_maxi_num": gr_maxi,
"gr_nb_notes": len(notes), "gr_nb_notes": len(notes),
"gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]), "gr_nb_att": len([x for x in notes if x == scu.NOTES_ATTENTE]),
} }
) )
gr_moyennes.sort(key=operator.itemgetter("group_name")) gr_moyennes.sort(key=operator.itemgetter("group_name"))
# retourne mapping
return { return {
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
"nb_inscrits": nb_inscrits, "nb_inscrits": nb_inscrits,
@ -256,14 +260,11 @@ def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=
"nb_abs": nb_abs, "nb_abs": nb_abs,
"nb_neutre": nb_neutre, "nb_neutre": nb_neutre,
"nb_att": nb_att, "nb_att": nb_att,
"moy": moy, "moy": moy, # chaine formattée, sur 20
"moy_num": moy_num,
"median": median, "median": median,
"mini": mini, "mini": mini,
"mini_num": mini_num,
"maxi": maxi, "maxi": maxi,
"maxi_num": maxi_num, "maxi_num": maxi_num, # note maximale, en nombre
"median_num": median_num,
"last_modif": last_modif, "last_modif": last_modif,
"gr_incomplets": gr_incomplets, "gr_incomplets": gr_incomplets,
"gr_moyennes": gr_moyennes, "gr_moyennes": gr_moyennes,
@ -283,18 +284,19 @@ def do_evaluation_list_in_sem(formsemestre_id, with_etat=True):
[ { [ {
'coefficient': 1.0, 'coefficient': 1.0,
'description': 'QCM et cas pratiques', 'description': 'QCM et cas pratiques',
'etat': {'evalattente': False, 'etat': {
'evalattente': False,
'evalcomplete': True, 'evalcomplete': True,
'evaluation_id': 'GEAEVAL82883', 'evaluation_id': 'GEAEVAL82883',
'gr_incomplets': [], 'gr_incomplets': [],
'gr_moyennes': [{'gr_median': '12.00', 'gr_moyennes': [{
'gr_median_num' : 12., 'gr_median': '12.00', # sur 20
'gr_moy': '11.88', 'gr_moy': '11.88',
'gr_moy_num' : 11.88,
'gr_nb_att': 0, 'gr_nb_att': 0,
'gr_nb_notes': 166, 'gr_nb_notes': 166,
'group_id': 'GEAG266762', 'group_id': 'GEAG266762',
'group_name': None}], 'group_name': None
}],
'groups': {'GEAG266762': {'etudid': 'GEAEID80603', 'groups': {'GEAG266762': {'etudid': 'GEAEID80603',
'group_id': 'GEAG266762', 'group_id': 'GEAG266762',
'group_name': None, 'group_name': None,
@ -362,7 +364,7 @@ def _eval_etat(evals):
if last_modif is not None: if last_modif is not None:
dates.append(e["etat"]["last_modif"]) dates.append(e["etat"]["last_modif"])
if len(dates): if dates:
dates = scu.sort_dates(dates) dates = scu.sort_dates(dates)
last_modif = dates[-1] # date de derniere modif d'une note dans un module last_modif = dates[-1] # date de derniere modif d'une note dans un module
else: else:

View File

@ -646,16 +646,23 @@ def formsemestre_description_table(
ects_str = ue.ects ects_str = ue.ects
ue_info = { ue_info = {
"UE": ue.acronyme, "UE": ue.acronyme,
"Code": "",
"ects": ects_str, "ects": ects_str,
"Module": ue.titre, "Module": ue.titre,
"_css_row_class": "table_row_ue", "_css_row_class": "table_row_ue",
"_UE_td_attrs": f'style="background-color: {ue.color} !important;"'
if ue.color
else "",
} }
if use_ue_coefs: if use_ue_coefs:
ue_info["Coef."] = ue.coefficient ue_info["Coef."] = ue.coefficient
ue_info["Coef._class"] = "ue_coef" ue_info["Coef._class"] = "ue_coef"
if ue.color:
for k in list(ue_info.keys()):
if not k.startswith("_"):
ue_info[
f"_{k}_td_attrs"
] = f'style="background-color: {ue.color} !important;"'
if not formsemestre.formation.is_apc():
# n'affiche la ligne UE qu'en formation classique
# car l'UE de rattachement n'a pas d'intérêt en BUT
rows.append(ue_info) rows.append(ue_info)
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list( mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
@ -663,9 +670,9 @@ def formsemestre_description_table(
) )
enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants) enseignants = ", ".join(ens.get_prenomnom() for ens in modimpl.enseignants)
l = { row = {
"UE": modimpl.module.ue.acronyme, "UE": modimpl.module.ue.acronyme,
"_UE_td_attrs": ue_info["_UE_td_attrs"], "_UE_td_attrs": ue_info.get("_UE_td_attrs", ""),
"Code": modimpl.module.code or "", "Code": modimpl.module.code or "",
"Module": modimpl.module.abbrev or modimpl.module.titre, "Module": modimpl.module.abbrev or modimpl.module.titre,
"_Module_class": "scotext", "_Module_class": "scotext",
@ -692,26 +699,32 @@ def formsemestre_description_table(
sum_coef += modimpl.module.coefficient sum_coef += modimpl.module.coefficient
coef_dict = modimpl.module.get_ue_coef_dict() coef_dict = modimpl.module.get_ue_coef_dict()
for ue in ues: for ue in ues:
l[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or "" row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
if with_parcours: if with_parcours:
l["parcours"] = ", ".join( row["parcours"] = ", ".join(
sorted([pa.code for pa in modimpl.module.parcours]) sorted([pa.code for pa in modimpl.module.parcours])
) )
rows.append(l) rows.append(row)
if with_evals: if with_evals:
# Ajoute lignes pour evaluations # Ajoute lignes pour evaluations
evals = nt.get_mod_evaluation_etat_list(modimpl.id) evals = nt.get_mod_evaluation_etat_list(modimpl.id)
evals.reverse() # ordre chronologique evals.reverse() # ordre chronologique
# Ajoute etat: # Ajoute etat:
eval_rows = []
for eval_dict in evals: for eval_dict in evals:
e = eval_dict.copy() e = eval_dict.copy()
e["_description_target"] = url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e["evaluation_id"],
)
e["_jour_order"] = e["jour"].isoformat() e["_jour_order"] = e["jour"].isoformat()
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else "" e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
e["UE"] = l["UE"] e["UE"] = row["UE"]
e["_UE_td_attrs"] = l["_UE_td_attrs"] e["_UE_td_attrs"] = row["_UE_td_attrs"]
e["Code"] = l["Code"] e["Code"] = row["Code"]
e["_css_row_class"] = "evaluation" e["_css_row_class"] = "evaluation"
e["Module"] = "éval." e["Module"] = "éval."
# Cosmetic: conversions pour affichage # Cosmetic: conversions pour affichage
@ -734,8 +747,9 @@ def formsemestre_description_table(
e[f"ue_{ue_id}"] = poids or "" e[f"ue_{ue_id}"] = poids or ""
e[f"_ue_{ue_id}_class"] = "poids" e[f"_ue_{ue_id}_class"] = "poids"
e[f"_ue_{ue_id}_help"] = "poids vers l'UE" e[f"_ue_{ue_id}_help"] = "poids vers l'UE"
eval_rows.append(e)
rows += evals rows += eval_rows
sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef} sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
rows.append(sums) rows.append(sums)

View File

@ -207,6 +207,7 @@ PREF_CATEGORIES = (
("apc", {"title": "BUT et Approches par Compétences"}), ("apc", {"title": "BUT et Approches par Compétences"}),
("abs", {"title": "Suivi des absences", "related": ("bul",)}), ("abs", {"title": "Suivi des absences", "related": ("bul",)}),
("portal", {"title": "Liaison avec portail (Apogée, etc)"}), ("portal", {"title": "Liaison avec portail (Apogée, etc)"}),
("apogee", {"title": "Exports Apogée"}),
( (
"pdf", "pdf",
{ {
@ -745,7 +746,7 @@ class BasePreferences(object):
"explanation": "remplissage maquettes export Apogée", "explanation": "remplissage maquettes export Apogée",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "portal", "category": "apogee",
"only_global": True, "only_global": True,
}, },
), ),
@ -757,7 +758,7 @@ class BasePreferences(object):
"explanation": "remplissage maquettes export Apogée", "explanation": "remplissage maquettes export Apogée",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "portal", "category": "apogee",
"only_global": True, "only_global": True,
}, },
), ),
@ -769,7 +770,7 @@ class BasePreferences(object):
"explanation": "remplissage maquettes export Apogée", "explanation": "remplissage maquettes export Apogée",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "portal", "category": "apogee",
"only_global": True, "only_global": True,
}, },
), ),
@ -781,7 +782,7 @@ class BasePreferences(object):
"explanation": "remplissage maquettes export Apogée", "explanation": "remplissage maquettes export Apogée",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "portal", "category": "apogee",
"only_global": True, "only_global": True,
}, },
), ),
@ -793,7 +794,7 @@ class BasePreferences(object):
"explanation": "si coché, exporte exporte étudiants même si pas décision de jury saisie (sinon laisse vide)", "explanation": "si coché, exporte exporte étudiants même si pas décision de jury saisie (sinon laisse vide)",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "portal", "category": "apogee",
"only_global": True, "only_global": True,
}, },
), ),
@ -805,7 +806,19 @@ class BasePreferences(object):
"explanation": "si coché, exporte exporte étudiants en attente de ratrapage comme ATT (sinon laisse vide)", "explanation": "si coché, exporte exporte étudiants en attente de ratrapage comme ATT (sinon laisse vide)",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"labels": ["non", "oui"], "labels": ["non", "oui"],
"category": "portal", "category": "apogee",
"only_global": True,
},
),
(
"export_res_remove_typ_res",
{
"initvalue": 0,
"title": "Ne pas recopier la section APO_TYP_RES",
"explanation": "si coché, ne réécrit pas la section APO_TYP_RES (rarement utile, utiliser avec précaution)",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"category": "apogee",
"only_global": True, "only_global": True,
}, },
), ),

View File

@ -351,7 +351,7 @@ def check_modif_user(
# Unicité du cas_id # Unicité du cas_id
if cas_id: if cas_id:
cas_users = User.query.filter_by(cas_id=cas_id).all() cas_users = User.query.filter_by(cas_id=str(cas_id)).all()
if edit: if edit:
if cas_users and ( if cas_users and (
len(cas_users) > 1 or cas_users[0].user_name != user_name len(cas_users) > 1 or cas_users[0].user_name != user_name

View File

@ -19,7 +19,6 @@ After=network.target
User=scodoc User=scodoc
Group=scodoc Group=scodoc
WorkingDirectory=/opt/scodoc WorkingDirectory=/opt/scodoc
#Environment=FLASK_ENV=production
ExecStart=/opt/scodoc/venv/bin/gunicorn -b localhost:8000 -w 4 --timeout 600 scodoc:app ExecStart=/opt/scodoc/venv/bin/gunicorn -b localhost:8000 -w 4 --timeout 600 scodoc:app
Restart=always Restart=always