diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index f57e22543..bc6dca945 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -228,7 +228,7 @@ class BonusSportAdditif(BonusSport):
axis=1,
)
# Seuil: bonus dans [min, max] (défaut [0,20])
- bonus_max = self.bonus_max or 0.0
+ bonus_max = self.bonus_max or 20.0
np.clip(bonus_moy_arr, self.bonus_min, bonus_max, out=bonus_moy_arr)
self.bonus_additif(bonus_moy_arr)
@@ -418,17 +418,46 @@ class BonusAmiens(BonusSportAdditif):
class BonusBethune(BonusSportMultiplicatif):
- """Calcul bonus modules optionnels (sport), règle IUT de Béthune.
-
- Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
- Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
- moyenne générale du semestre de l'étudiant.
+ """
+ Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
+
+ Pour le BUT :
+ La note de sport est sur 20, et on calcule une bonification (en %)
+ qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
+ la formule : bonification (en %) = max(note-10, 0)*(1/500).
+
+ La bonification ne s'applique que si la note est supérieure à 10.
+
+ (Une note de 10 donne donc 0% de bonif,
+ 1 point au dessus de 10 augmente la moyenne des UE de 0.2%)
+
+
+ Pour le DUT/LP :
+ La note de sport est sur 20, et on calcule une bonification (en %)
+ qui va s'appliquer à la moyenne générale du semestre en appliquant
+ la formule : bonification (en %) = max(note-10, 0)*(1/200).
+
+ La bonification ne s'applique que si la note est supérieure à 10.
+
+ (Une note de 10 donne donc 0% de bonif,
+ 1 point au dessus de 10 augmente la moyenne des UE de 0.5%)
+
"""
name = "bonus_iutbethune"
displayed_name = "IUT de Béthune"
- seuil_moy_gen = 10.0
- amplitude = 0.005
+ seuil_moy_gen = 10.0 # points comptés au dessus de 10.
+
+ def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
+ """calcul du bonus"""
+ if self.formsemestre.formation.is_apc():
+ self.amplitude = 0.002
+ else:
+ self.amplitude = 0.005
+
+ return super().compute_bonus(
+ sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
+ )
class BonusBezier(BonusSportAdditif):
@@ -502,10 +531,11 @@ class BonusCachan1(BonusSportAdditif):
- DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
- bonifie les moyennes d'UE (sauf l'UE41 dont le code est UE41_E) à raison
+ bonifie les moyennes d'UE (uniquement UE13_E pour le semestre 1, UE23_E
+ pour le semestre 2, UE33_E pour le semestre 3 et UE43_E pour le semestre
+ 4) à raison
de bonus = (option - 10)/10.
-
- BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
les moyennes d'UE à raison de bonus = (option - 10) * 3%.
@@ -516,6 +546,7 @@ class BonusCachan1(BonusSportAdditif):
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.03
classic_use_bonus_ues = True
+ ues_bonifiables_cachan = {"UE13_E", "UE23_E", "UE33_E", "UE43_E"}
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant le type de formation"""
@@ -540,7 +571,7 @@ class BonusCachan1(BonusSportAdditif):
dtype=float,
)
else: # --- DUT
- # pareil mais proportion différente et exclusion d'une UE
+ # pareil mais proportion différente et application à certaines UEs
proportion_point = 0.1
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
@@ -553,10 +584,10 @@ class BonusCachan1(BonusSportAdditif):
columns=ues_idx,
dtype=float,
)
- # Pas de bonus sur la ou les ue de code "UE41_E"
- ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"]
- for ue in ue_exclues:
- self.bonus_ues[ue.id] = 0.0
+ # Applique bonus seulement sur certaines UE de code connu:
+ for ue in ues:
+ if ue.ue_code not in self.ues_bonifiables_cachan:
+ self.bonus_ues[ue.id] = 0.0 # annule
class BonusCalais(BonusSportAdditif):
@@ -982,7 +1013,7 @@ class BonusTarbes(BonusSportAdditif):
"""
name = "bonus_tarbes"
- displayed_name = "IUT de Tazrbes"
+ displayed_name = "IUT de Tarbes"
seuil_moy_gen = 10.0
proportion_point = 1 / 30.0
classic_use_bonus_ues = True
diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py
index eea357e8f..13829a392 100644
--- a/app/comp/moy_mod.py
+++ b/app/comp/moy_mod.py
@@ -92,6 +92,8 @@ class ModuleImplResults:
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE.
"""
+ self.evals_etudids_sans_note = {}
+ """dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
self.load_notes()
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
@@ -142,12 +144,13 @@ class ModuleImplResults:
# ou évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes
# car on calcule leur moyenne à part.
+ etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
- evaluation.publish_incomplete
- or (not (inscrits_module - set(eval_df.index)))
+ evaluation.publish_incomplete or (not etudids_sans_note)
)
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
+ self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
@@ -193,7 +196,9 @@ class ModuleImplResults:
return eval_df
def _etudids(self):
- """L'index du dataframe est la liste de tous les étudiants inscrits au semestre"""
+ """L'index du dataframe est la liste de tous les étudiants inscrits au semestre
+ (incluant les DEM et DEF)
+ """
return [
inscr.etudid
for inscr in ModuleImpl.query.get(
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index 1f19fbfd9..7bc199ead 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -14,18 +14,19 @@ import pandas as pd
from flask import g, url_for
+from app.auth.models import User
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
-from app.models import FormSemestre, FormSemestreUECoef
+from app.models import FormSemestre, FormSemestreUECoef, formsemestre
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF, DEM
+from app.scodoc import sco_evaluation_db
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_groups
-from app.scodoc import sco_users
from app.scodoc import sco_utils as scu
# Il faut bien distinguer
@@ -387,7 +388,9 @@ class ResultatsSemestre(ResultatsCache):
# --- TABLEAU RECAP
- def get_table_recap(self, convert_values=False):
+ def get_table_recap(
+ self, convert_values=False, include_evaluations=False, modejury=False
+ ):
"""Result: tuple avec
- rows: liste de dicts { column_id : value }
- titles: { column_id : title }
@@ -413,7 +416,6 @@ class ResultatsSemestre(ResultatsCache):
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_
__order : clé de tri
"""
-
if convert_values:
fmt_note = scu.fmt_note
else:
@@ -429,6 +431,7 @@ class ResultatsSemestre(ResultatsCache):
titles = {}
# les titres en footer: les mêmes, mais avec des bulles et liens:
titles_bot = {}
+ dict_nom_res = {} # cache uid : nomcomplet
def add_cell(
row: dict,
@@ -457,6 +460,11 @@ class ResultatsSemestre(ResultatsCache):
idx = 0 # index de la colonne
etud = Identite.query.get(etudid)
row = {"etudid": etudid}
+ # --- Codes (seront cachés, mais exportés en excel)
+ idx = add_cell(row, "etudid", "etudid", etudid, "codes", idx)
+ idx = add_cell(
+ row, "code_nip", "code_nip", etud.code_nip or "", "codes", idx
+ )
# --- Rang
idx = add_cell(
row, "rang", "Rg", self.etud_moy_gen_ranks[etudid], "rang", idx
@@ -532,22 +540,28 @@ class ResultatsSemestre(ResultatsCache):
titles_bot[
f"_{col_id}_target_attrs"
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
+ if modejury:
+ # pas d'autre colonnes de résultats
+ continue
# Bonus (sport) dans cette UE ?
# Le bonus sport appliqué sur cette UE
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
val = self.bonus_ues[ue.id][etud.id] or ""
- val_fmt = fmt_note(val)
+ val_fmt = val_fmt_html = fmt_note(val)
if val:
- val_fmt = f'{val_fmt}'
+ val_fmt_html = f'{val_fmt}'
idx = add_cell(
row,
f"bonus_ue_{ue.id}",
f"Bonus {ue.acronyme}",
- val_fmt,
+ val_fmt_html,
"col_ue_bonus",
idx,
)
+ row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
+ idx_malus = idx # place pour colonne malus à gauche des modules
+ idx += 1
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
if ue_status["is_capitalized"]:
val = "-c-"
@@ -573,63 +587,87 @@ class ResultatsSemestre(ResultatsCache):
col_id = (
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
)
- val_fmt = fmt_note(val)
+ val_fmt = val_fmt_html = fmt_note(val)
if modimpl.module.module_type == scu.ModuleType.MALUS:
- val_fmt = (
+ val_fmt_html = (
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
)
idx = add_cell(
row,
col_id,
modimpl.module.code,
- val_fmt,
+ val_fmt_html,
# class col_res mod_ue_123
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
idx,
)
+ row[f"_{col_id}_xls"] = val_fmt
+ if modimpl.module.module_type == scu.ModuleType.MALUS:
+ titles[f"_{col_id}_col_order"] = idx_malus
titles_bot[f"_{col_id}_target"] = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
+ nom_resp = dict_nom_res.get(modimpl.responsable_id)
+ if nom_resp is None:
+ user = User.query.get(modimpl.responsable_id)
+ nom_resp = user.get_nomcomplet() if user else ""
+ dict_nom_res[modimpl.responsable_id] = nom_resp
titles_bot[
f"_{col_id}_target_attrs"
- ] = f"""
- title="{modimpl.module.titre}
- ({sco_users.user_info(modimpl.responsable_id)['nomcomplet']})" """
+ ] = f""" title="{modimpl.module.titre} ({nom_resp})" """
modimpl_ids.add(modimpl.id)
- ue_valid_txt = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
+ ue_valid_txt = (
+ ue_valid_txt_html
+ ) = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
if nb_ues_warning:
- ue_valid_txt += " " + scu.EMO_WARNING
+ ue_valid_txt_html += " " + scu.EMO_WARNING
add_cell(
row,
"ues_validables",
"UEs",
- ue_valid_txt,
+ ue_valid_txt_html,
"col_ue col_ues_validables",
29, # juste avant moy. gen.
)
+ row["_ues_validables_xls"] = ue_valid_txt
if nb_ues_warning:
row["_ues_validables_class"] += " moy_ue_warning"
elif nb_ues_validables < len(ues_sans_bonus):
row["_ues_validables_class"] += " moy_inf"
row["_ues_validables_order"] = nb_ues_validables # pour tri
+ if modejury:
+ idx = add_cell(
+ row,
+ "jury_link",
+ "",
+ f"""saisir décision""",
+ "col_jury_link",
+ 1000,
+ )
rows.append(row)
self._recap_add_partitions(rows, titles)
self._recap_add_admissions(rows, titles)
+
# tri par rang croissant
rows.sort(key=lambda e: e["_rang_order"])
# INFOS POUR FOOTER
bottom_infos = self._recap_bottom_infos(ues_sans_bonus, modimpl_ids, fmt_note)
+ if include_evaluations:
+ self._recap_add_evaluations(rows, titles, bottom_infos)
# Ajoute style "col_empty" aux colonnes de modules vides
for col_id in titles:
c_class = f"_{col_id}_class"
if "col_empty" in bottom_infos["moy"].get(c_class, ""):
for row in rows:
- row[c_class] += " col_empty"
+ row[c_class] = row.get(c_class, "") + " col_empty"
titles[c_class] += " col_empty"
for row in bottom_infos.values():
row[c_class] = row.get(c_class, "") + " col_empty"
@@ -641,7 +679,9 @@ class ResultatsSemestre(ResultatsCache):
row["moy_gen"] = row.get("moy_gen", "")
row["_moy_gen_class"] = "col_moy_gen"
# titre de la ligne:
- row["prenom"] = row["nom_short"] = bottom_line.capitalize()
+ row["prenom"] = row["nom_short"] = (
+ row.get("_title", "") or bottom_line.capitalize()
+ )
row["_tr_class"] = bottom_line.lower() + (
(" " + row["_tr_class"]) if "_tr_class" in row else ""
)
@@ -656,53 +696,58 @@ class ResultatsSemestre(ResultatsCache):
def _recap_bottom_infos(self, ues, modimpl_ids: set, fmt_note) -> dict:
"""Les informations à mettre en bas de la table: min, max, moy, ECTS"""
- row_min, row_max, row_moy, row_coef, row_ects = (
- {"_tr_class": "bottom_info"},
+ row_min, row_max, row_moy, row_coef, row_ects, row_apo = (
+ {"_tr_class": "bottom_info", "_title": "Min."},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"},
{"_tr_class": "bottom_info"},
+ {"_tr_class": "bottom_info", "_title": "Code Apogée"},
)
# --- ECTS
for ue in ues:
- row_ects[f"moy_ue_{ue.id}"] = ue.ects
- row_ects[f"_moy_ue_{ue.id}_class"] = "col_ue"
+ colid = f"moy_ue_{ue.id}"
+ row_ects[colid] = ue.ects
+ row_ects[f"_{colid}_class"] = "col_ue"
# style cases vides pour borders verticales
- row_coef[f"moy_ue_{ue.id}"] = ""
- row_coef[f"_moy_ue_{ue.id}_class"] = "col_ue"
+ row_coef[colid] = ""
+ row_coef[f"_{colid}_class"] = "col_ue"
+ # row_apo[colid] = ue.code_apogee or ""
row_ects["moy_gen"] = sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT])
row_ects["_moy_gen_class"] = "col_moy_gen"
- # --- MIN, MAX, MOY
+ # --- MIN, MAX, MOY, APO
row_min["moy_gen"] = fmt_note(self.etud_moy_gen.min())
row_max["moy_gen"] = fmt_note(self.etud_moy_gen.max())
row_moy["moy_gen"] = fmt_note(self.etud_moy_gen.mean())
for ue in ues:
- col_id = f"moy_ue_{ue.id}"
- row_min[col_id] = fmt_note(self.etud_moy_ue[ue.id].min())
- row_max[col_id] = fmt_note(self.etud_moy_ue[ue.id].max())
- row_moy[col_id] = fmt_note(self.etud_moy_ue[ue.id].mean())
- row_min[f"_{col_id}_class"] = "col_ue"
- row_max[f"_{col_id}_class"] = "col_ue"
- row_moy[f"_{col_id}_class"] = "col_ue"
+ colid = f"moy_ue_{ue.id}"
+ row_min[colid] = fmt_note(self.etud_moy_ue[ue.id].min())
+ row_max[colid] = fmt_note(self.etud_moy_ue[ue.id].max())
+ row_moy[colid] = fmt_note(self.etud_moy_ue[ue.id].mean())
+ row_min[f"_{colid}_class"] = "col_ue"
+ row_max[f"_{colid}_class"] = "col_ue"
+ row_moy[f"_{colid}_class"] = "col_ue"
+ row_apo[colid] = ue.code_apogee or ""
for modimpl in self.formsemestre.modimpls_sorted:
if modimpl.id in modimpl_ids:
- col_id = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
+ colid = f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
if self.is_apc:
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
else:
coef = modimpl.module.coefficient or 0
- row_coef[col_id] = fmt_note(coef)
+ row_coef[colid] = fmt_note(coef)
notes = self.modimpl_notes(modimpl.id, ue.id)
- row_min[col_id] = fmt_note(np.nanmin(notes))
- row_max[col_id] = fmt_note(np.nanmax(notes))
+ row_min[colid] = fmt_note(np.nanmin(notes))
+ row_max[colid] = fmt_note(np.nanmax(notes))
moy = np.nanmean(notes)
- row_moy[col_id] = fmt_note(moy)
+ row_moy[colid] = fmt_note(moy)
if np.isnan(moy):
# aucune note dans ce module
- row_moy[f"_{col_id}_class"] = "col_empty"
+ row_moy[f"_{colid}_class"] = "col_empty"
+ row_apo[colid] = modimpl.module.code_apogee or ""
return { # { key : row } avec key = min, max, moy, coef
"min": row_min,
@@ -710,6 +755,7 @@ class ResultatsSemestre(ResultatsCache):
"moy": row_moy,
"coef": row_coef,
"ects": row_ects,
+ "apo": row_apo,
}
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
@@ -803,3 +849,68 @@ class ResultatsSemestre(ResultatsCache):
row[f"{cid}"] = gr_name
row[f"_{cid}_class"] = klass
first_partition = False
+
+ def _recap_add_evaluations(
+ self, rows: list[dict], titles: dict, bottom_infos: dict
+ ):
+ """Ajoute les colonnes avec les notes aux évaluations
+ rows est une liste de dict avec une clé "etudid"
+ Les colonnes ont la classe css "evaluation"
+ """
+ # nouvelle ligne pour description évaluations:
+ bottom_infos["descr_evaluation"] = {
+ "_tr_class": "bottom_info",
+ "_title": "Description évaluation",
+ }
+ first_eval = True
+ index_col = 9000 # à droite
+ for modimpl in self.formsemestre.modimpls_sorted:
+ evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
+ eval_index = len(evals) - 1
+ inscrits = {i.etudid for i in modimpl.inscriptions}
+ first_eval_of_mod = True
+ for e in evals:
+ cid = f"eval_{e.id}"
+ titles[
+ cid
+ ] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
+ klass = "evaluation"
+ if first_eval:
+ klass += " first"
+ elif first_eval_of_mod:
+ klass += " first_of_mod"
+ titles[f"_{cid}_class"] = klass
+ first_eval_of_mod = first_eval = False
+ titles[f"_{cid}_col_order"] = index_col
+ index_col += 1
+ eval_index -= 1
+ notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
+ e.evaluation_id
+ )
+ for row in rows:
+ etudid = row["etudid"]
+ if etudid in inscrits:
+ if etudid in notes_db:
+ val = notes_db[etudid]["value"]
+ else:
+ # Note manquante mais prise en compte immédiate: affiche ATT
+ val = scu.NOTES_ATTENTE
+ row[cid] = scu.fmt_note(val)
+ row[f"_{cid}_class"] = klass + {
+ "ABS": " abs",
+ "ATT": " att",
+ "EXC": " exc",
+ }.get(row[cid], "")
+ else:
+ row[cid] = "ni"
+ row[f"_{cid}_class"] = klass + " non_inscrit"
+
+ bottom_infos["coef"][cid] = e.coefficient
+ bottom_infos["min"][cid] = "0"
+ bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
+ bottom_infos["descr_evaluation"][cid] = e.description or ""
+ bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for(
+ "notes.evaluation_listenotes",
+ scodoc_dept=g.scodoc_dept,
+ evaluation_id=e.id,
+ )
diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py
index facbf85fe..9a5e11989 100644
--- a/app/forms/main/config_apo.py
+++ b/app/forms/main/config_apo.py
@@ -70,5 +70,16 @@ class CodesDecisionsForm(FlaskForm):
DEM = _build_code_field("DEM")
NAR = _build_code_field("NAR")
RAT = _build_code_field("RAT")
+ NOTES_FMT = StringField(
+ label="Format notes exportées",
+ description="""Format des notes. Par défaut %3.2f (deux chiffres après la virgule)""",
+ validators=[
+ validators.Length(
+ max=SHORT_STR_LEN,
+ message=f"Le format ne doit pas dépasser {SHORT_STR_LEN} caractères",
+ ),
+ validators.DataRequired("format requis"),
+ ],
+ )
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
diff --git a/app/models/config.py b/app/models/config.py
index 1271beeb9..53ac96e9b 100644
--- a/app/models/config.py
+++ b/app/models/config.py
@@ -36,6 +36,7 @@ CODES_SCODOC_TO_APO = {
DEM: "NAR",
NAR: "NAR",
RAT: "ATT",
+ "NOTES_FMT": "%3.2f",
}
@@ -157,32 +158,6 @@ class ScoDocSiteConfig(db.Model):
class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
return [("", "")] + class_list
- @classmethod
- def get_bonus_sport_func(cls):
- """Fonction bonus_sport ScoDoc 7 XXX
- Transitoire pour les tests durant la transition #sco92
- """
- """returns bonus func with specified name.
- If name not specified, return the configured function.
- None if no bonus function configured.
- Raises ScoValueError if func_name not found in module bonus_sport.
- """
- from app.scodoc import bonus_sport
-
- c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
- if c is None:
- return None
- func_name = c.value
- if func_name == "": # pas de bonus défini
- return None
- try:
- return getattr(bonus_sport, func_name)
- except AttributeError:
- raise ScoValueError(
- f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}".
- (contacter votre administrateur local)."""
- )
-
@classmethod
def get_code_apo(cls, code: str) -> str:
"""La représentation d'un code pour les exports Apogée.
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 6c342482c..962e7baaf 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -16,6 +16,7 @@ from app import models
from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat
+from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
@@ -354,7 +355,10 @@ def make_etud_args(
"""
args = None
if etudid:
- args = {"etudid": etudid}
+ try:
+ args = {"etudid": int(etudid)}
+ except ValueError as exc:
+ raise ScoValueError("Adresse invalide") from exc
elif code_nip:
args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global)
diff --git a/app/models/groups.py b/app/models/groups.py
index f6452cf7c..9cf5f2364 100644
--- a/app/models/groups.py
+++ b/app/models/groups.py
@@ -74,6 +74,10 @@ class GroupDescr(db.Model):
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
)
+ def get_nom_with_part(self) -> str:
+ "Nom avec partition: 'TD A'"
+ return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
+
group_membership = db.Table(
"group_membership",
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index 292ec8ffd..7574ed7e8 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -9,7 +9,6 @@ from app.comp import df_cache
from app.models.etudiants import Identite
from app.models.modules import Module
-import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu
diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py
index 7f5531c6f..2136ee841 100644
--- a/app/scodoc/gen_tables.py
+++ b/app/scodoc/gen_tables.py
@@ -209,7 +209,8 @@ class GenTable(object):
omit_hidden_lines=False,
pdf_mode=False, # apply special pdf reportlab processing
pdf_style_list=[], # modified: list of platypus table style commands
- ):
+ xls_mode=False, # get xls content if available
+ ) -> list:
"table data as a list of lists (rows)"
T = []
line_num = 0 # line number in input data
@@ -237,9 +238,14 @@ class GenTable(object):
# if colspan_count > 0:
# continue # skip cells after a span
if pdf_mode:
- content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
+ content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
+ elif xls_mode:
+ content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
else:
- content = row.get(cid, "") or "" # nota: None converted to ''
+ content = row.get(cid, "")
+ # Convert None to empty string ""
+ content = "" if content is None else content
+
colspan = row.get("_%s_colspan" % cid, 0)
if colspan > 1:
pdf_style_list.append(
@@ -299,7 +305,7 @@ class GenTable(object):
return self.xml()
elif format == "json":
return self.json()
- raise ValueError("GenTable: invalid format: %s" % format)
+ raise ValueError(f"GenTable: invalid format: {format}")
def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
"row is a dict, returns a string ..."
@@ -479,23 +485,23 @@ class GenTable(object):
def excel(self, wb=None):
"""Simple Excel representation of the table"""
if wb is None:
- ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
+ sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
else:
- ses = wb.create_sheet(sheet_name=self.xls_sheet_name)
- ses.rows += self.xls_before_table
+ sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
+ sheet.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True)
style_base = sco_excel.excel_make_style()
- ses.append_row(ses.make_row(self.get_titles_list(), style_bold))
- for line in self.get_data_list():
- ses.append_row(ses.make_row(line, style_base))
+ sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
+ for line in self.get_data_list(xls_mode=True):
+ sheet.append_row(sheet.make_row(line, style_base))
if self.caption:
- ses.append_blank_row() # empty line
- ses.append_single_cell_row(self.caption, style_base)
+ sheet.append_blank_row() # empty line
+ sheet.append_single_cell_row(self.caption, style_base)
if self.origin:
- ses.append_blank_row() # empty line
- ses.append_single_cell_row(self.origin, style_base)
+ sheet.append_blank_row() # empty line
+ sheet.append_single_cell_row(self.origin, style_base)
if wb is None:
- return ses.generate()
+ return sheet.generate()
def text(self):
"raw text representation of the table"
diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py
index 53efbfc95..c700c1b80 100644
--- a/app/scodoc/html_sidebar.py
+++ b/app/scodoc/html_sidebar.py
@@ -86,9 +86,9 @@ def sidebar():
f"""