1124 lines
46 KiB
Python
1124 lines
46 KiB
Python
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""Résultats semestre: méthodes communes aux formations classiques et APC
|
|
"""
|
|
|
|
from collections import Counter
|
|
from collections.abc import Generator
|
|
from functools import cached_property
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from flask import g, url_for
|
|
|
|
from app.auth.models import User
|
|
from app.comp import res_sem
|
|
from app.comp.res_cache import ResultatsCache
|
|
from app.comp.jury import ValidationsSemestre
|
|
from app.comp.moy_mod import ModuleImplResults
|
|
from app.models import FormSemestre, FormSemestreUECoef
|
|
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_utils as scu
|
|
from app.scodoc import table_builder as tb
|
|
|
|
# Il faut bien distinguer
|
|
# - ce qui est caché de façon persistente (via redis):
|
|
# ce sont les attributs listés dans `_cached_attrs`
|
|
# le stockage et l'invalidation sont gérés dans sco_cache.py
|
|
#
|
|
# - les valeurs cachées durant le temps d'une requête
|
|
# (durée de vie de l'instance de ResultatsSemestre)
|
|
# qui sont notamment les attributs décorés par `@cached_property``
|
|
#
|
|
class ResultatsSemestre(ResultatsCache):
|
|
"""Les résultats (notes, ...) d'un formsemestre
|
|
Classe commune à toutes les formations (classiques, BUT)
|
|
"""
|
|
|
|
_cached_attrs = (
|
|
"bonus",
|
|
"bonus_ues",
|
|
"dispense_ues",
|
|
"etud_coef_ue_df",
|
|
"etud_moy_gen_ranks",
|
|
"etud_moy_gen",
|
|
"etud_moy_ue",
|
|
"modimpl_inscr_df",
|
|
"modimpls_results",
|
|
"moyennes_matieres",
|
|
)
|
|
|
|
def __init__(self, formsemestre: FormSemestre):
|
|
super().__init__(formsemestre, ResultatsSemestreCache)
|
|
# BUT ou standard ? (apc == "approche par compétences")
|
|
self.is_apc = formsemestre.formation.is_apc()
|
|
# Attributs "virtuels", définis dans les sous-classes
|
|
self.bonus: pd.Series = None # virtuel
|
|
"Bonus sur moy. gen. Series de float, index etudid"
|
|
self.bonus_ues: pd.DataFrame = None # virtuel
|
|
"DataFrame de float, index etudid, columns: ue.id"
|
|
self.dispense_ues: set[tuple[int, int]] = set()
|
|
"""set des dispenses d'UE: (etudid, ue_id), en APC seulement."""
|
|
# ResultatsSemestreBUT ou ResultatsSemestreClassic
|
|
self.etud_moy_ue = {}
|
|
"etud_moy_ue: DataFrame columns UE, rows etudid"
|
|
self.etud_moy_gen: pd.Series = None
|
|
self.etud_moy_gen_ranks = {}
|
|
self.etud_moy_gen_ranks_int = {}
|
|
self.moy_gen_rangs_by_group = None # virtual
|
|
self.modimpl_inscr_df: pd.DataFrame = None
|
|
"Inscriptions: row etudid, col modimlpl_id"
|
|
self.modimpls_results: ModuleImplResults = None
|
|
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
|
|
self.etud_coef_ue_df = None
|
|
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
|
|
self.modimpl_coefs_df: pd.DataFrame = None
|
|
"""Coefs APC: rows = UEs (sans bonus), columns = modimpl, value = coef."""
|
|
|
|
self.validations = None
|
|
self.moyennes_matieres = {}
|
|
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
|
|
|
|
def __repr__(self):
|
|
return f"<{self.__class__.__name__}(formsemestre='{self.formsemestre}')>"
|
|
|
|
def compute(self):
|
|
"Charge les notes et inscriptions et calcule toutes les moyennes"
|
|
# voir ce qui est chargé / calculé ici et dans les sous-classes
|
|
raise NotImplementedError()
|
|
|
|
def get_inscriptions_counts(self) -> Counter:
|
|
"""Nombre d'inscrits, défaillants, démissionnaires.
|
|
|
|
Exemple: res.get_inscriptions_counts()[scu.INSCRIT]
|
|
|
|
Result: a collections.Counter instance
|
|
"""
|
|
return Counter(ins.etat for ins in self.formsemestre.inscriptions)
|
|
|
|
@cached_property
|
|
def etuds(self) -> list[Identite]:
|
|
"Liste des inscrits au semestre, avec les démissionnaires et les défaillants"
|
|
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
|
|
return self.formsemestre.get_inscrits(include_demdef=True)
|
|
|
|
@cached_property
|
|
def etud_index(self) -> dict[int, int]:
|
|
"dict { etudid : indice dans les inscrits }"
|
|
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
|
|
|
def etud_ues_ids(self, etudid: int) -> list[int]:
|
|
"""Liste des UE auxquelles l'etudiant est inscrit, sans bonus
|
|
(surchargée en BUT pour prendre en compte les parcours)
|
|
"""
|
|
# Pour les formations classiques, etudid n'est pas utilisé
|
|
# car tous les étudiants sont inscrits à toutes les UE
|
|
return [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
|
|
|
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
|
"""Liste des UE auxquelles l'étudiant est inscrit."""
|
|
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
|
|
|
def etud_ects_tot_sem(self, etudid: int) -> float:
|
|
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
|
etud_ues = self.etud_ues(etudid)
|
|
return sum([ue.ects or 0 for ue in etud_ues]) if etud_ues else 0.0
|
|
|
|
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
|
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
|
Utile pour stats bottom tableau recap.
|
|
Résultat: 1d array of float
|
|
"""
|
|
# différent en BUT et classique: virtuelle
|
|
raise NotImplementedError
|
|
|
|
@cached_property
|
|
def etuds_dict(self) -> dict[int, Identite]:
|
|
"""dict { etudid : Identite } inscrits au semestre,
|
|
avec les démissionnaires et defs."""
|
|
return {etud.id: etud for etud in self.etuds}
|
|
|
|
@cached_property
|
|
def ues(self) -> list[UniteEns]:
|
|
"""Liste des UEs du semestre (avec les UE bonus sport)
|
|
(indices des DataFrames).
|
|
Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
|
|
"""
|
|
return self.formsemestre.query_ues(with_sport=True).all()
|
|
|
|
@cached_property
|
|
def ressources(self):
|
|
"Liste des ressources du semestre, triées par numéro de module"
|
|
return [
|
|
m
|
|
for m in self.formsemestre.modimpls_sorted
|
|
if m.module.module_type == scu.ModuleType.RESSOURCE
|
|
]
|
|
|
|
@cached_property
|
|
def saes(self):
|
|
"Liste des SAÉs du semestre, triées par numéro de module"
|
|
return [
|
|
m
|
|
for m in self.formsemestre.modimpls_sorted
|
|
if m.module.module_type == scu.ModuleType.SAE
|
|
]
|
|
|
|
# --- JURY...
|
|
def load_validations(self) -> ValidationsSemestre:
|
|
"""Load validations, set attribute and return value"""
|
|
if not self.validations:
|
|
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
|
return self.validations
|
|
|
|
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
|
|
"""Liste des UEs du semestre qui doivent être validées
|
|
|
|
Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
|
|
|
|
- En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
|
|
du parcours.
|
|
|
|
- En classique: toutes les UEs des modimpls auxquels l'étudiant est inscrit sont
|
|
susceptibles d'être validées.
|
|
|
|
Les UE "bonus" (sport) ne sont jamais "validables".
|
|
"""
|
|
if self.is_apc:
|
|
return list(self.etud_ues(etudid))
|
|
# Formations classiques:
|
|
# restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
|
|
ues = {
|
|
modimpl.module.ue
|
|
for modimpl in self.formsemestre.modimpls_sorted
|
|
if self.modimpl_inscr_df[modimpl.id][etudid]
|
|
}
|
|
ues = sorted(list(ues), key=lambda x: x.numero or 0)
|
|
return ues
|
|
|
|
def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
|
|
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit.
|
|
Utile en formations classiques, surchargée pour le BUT.
|
|
Inclus modules bonus le cas échéant.
|
|
"""
|
|
# Utilisée pour l'affichage ou non de l'UE sur le bulletin
|
|
# Méthode surchargée en BUT
|
|
modimpls = [
|
|
modimpl
|
|
for modimpl in self.formsemestre.modimpls_sorted
|
|
if modimpl.module.ue.id == ue.id
|
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
|
]
|
|
if not with_bonus:
|
|
return [
|
|
modimpl for modimpl in modimpls if modimpl.module.ue.type != UE_SPORT
|
|
]
|
|
return modimpls
|
|
|
|
@cached_property
|
|
def ue_au_dessus(self, seuil=10.0) -> pd.DataFrame:
|
|
"""DataFrame columns UE, rows etudid, valeurs: bool
|
|
Par exemple, pour avoir le nombre d'UE au dessus de 10 pour l'étudiant etudid
|
|
nb_ues_ok = sum(res.ue_au_dessus().loc[etudid])
|
|
"""
|
|
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
|
|
|
|
def apply_capitalisation(self):
|
|
"""Recalcule la moyenne générale pour prendre en compte d'éventuelles
|
|
UE capitalisées.
|
|
"""
|
|
# Supposant qu'il y a peu d'UE capitalisées,
|
|
# on recalcule les moyennes gen des etuds ayant des UE capitalisée.
|
|
self.load_validations()
|
|
ue_capitalisees = self.validations.ue_capitalisees
|
|
for etudid in ue_capitalisees.index:
|
|
recompute_mg = False
|
|
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])
|
|
# for ue_code in ue_codes:
|
|
# ue = ue_by_code.get(ue_code)
|
|
# if ue is None:
|
|
# ue = self.formsemestre.query_ues.filter_by(ue_code=ue_code)
|
|
# ue_by_code[ue_code] = ue
|
|
|
|
# Quand il y a une capitalisation, vérifie toutes les UEs
|
|
sum_notes_ue = 0.0
|
|
sum_coefs_ue = 0.0
|
|
for ue in self.formsemestre.query_ues():
|
|
ue_cap = self.get_etud_ue_status(etudid, ue.id)
|
|
if ue_cap is None:
|
|
continue
|
|
if ue_cap["is_capitalized"]:
|
|
recompute_mg = True
|
|
coef = ue_cap["coef_ue"]
|
|
if not np.isnan(ue_cap["moy"]) and coef:
|
|
sum_notes_ue += ue_cap["moy"] * coef
|
|
sum_coefs_ue += coef
|
|
|
|
if recompute_mg and sum_coefs_ue > 0.0:
|
|
# On doit prendre en compte une ou plusieurs UE capitalisées
|
|
# et donc recalculer la moyenne générale
|
|
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
|
|
# Ajoute le bonus sport
|
|
if self.bonus is not None and self.bonus[etudid]:
|
|
self.etud_moy_gen[etudid] += self.bonus[etudid]
|
|
self.etud_moy_gen[etudid] = max(
|
|
0.0, min(self.etud_moy_gen[etudid], 20.0)
|
|
)
|
|
|
|
def get_etud_etat(self, etudid: int) -> str:
|
|
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
|
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
|
|
if ins is None:
|
|
return ""
|
|
return ins.etat
|
|
|
|
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
|
|
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
|
|
Résultat:
|
|
Si pas capitalisée: None
|
|
Si capitalisée: un dict, avec les colonnes de validation.
|
|
"""
|
|
capitalisations = self.validations.ue_capitalisees.loc[etudid]
|
|
if isinstance(capitalisations, pd.DataFrame):
|
|
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
|
|
if ue_cap.empty:
|
|
return None
|
|
if isinstance(ue_cap, pd.DataFrame):
|
|
# si plusieurs fois capitalisée, prend le max
|
|
cap_idx = ue_cap["moy_ue"].values.argmax()
|
|
ue_cap = ue_cap.iloc[cap_idx]
|
|
else:
|
|
if capitalisations["ue_code"] == ue.ue_code:
|
|
ue_cap = capitalisations
|
|
else:
|
|
return None
|
|
# converti la Series en dict, afin que les np.int64 reviennent en int
|
|
# et remplace les NaN (venant des NULL en base) par des None
|
|
ue_cap_dict = ue_cap.to_dict()
|
|
if ue_cap_dict["formsemestre_id"] is not None and np.isnan(
|
|
ue_cap_dict["formsemestre_id"]
|
|
):
|
|
ue_cap_dict["formsemestre_id"] = None
|
|
if ue_cap_dict["compense_formsemestre_id"] is not None and np.isnan(
|
|
ue_cap_dict["compense_formsemestre_id"]
|
|
):
|
|
ue_cap_dict["compense_formsemestre_id"] = None
|
|
return ue_cap_dict
|
|
|
|
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
|
|
"""L'état de l'UE pour cet étudiant.
|
|
Result: dict, ou None si l'UE n'est pas dans ce semestre.
|
|
"""
|
|
ue = UniteEns.query.get(ue_id)
|
|
if ue.type == UE_SPORT:
|
|
return {
|
|
"is_capitalized": False,
|
|
"was_capitalized": False,
|
|
"is_external": False,
|
|
"coef_ue": 0.0,
|
|
"cur_moy_ue": 0.0,
|
|
"moy": 0.0,
|
|
"event_date": None,
|
|
"ue": ue.to_dict(),
|
|
"formsemestre_id": None,
|
|
"capitalized_ue_id": None,
|
|
"ects_pot": 0.0,
|
|
"ects": 0.0,
|
|
"ects_ue": ue.ects,
|
|
}
|
|
if not ue_id in self.etud_moy_ue:
|
|
return None
|
|
if not self.validations:
|
|
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
|
|
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
|
|
moy_ue = cur_moy_ue
|
|
is_capitalized = False # si l'UE prise en compte est une UE capitalisée
|
|
# s'il y a precedemment une UE capitalisée (pas forcement meilleure):
|
|
was_capitalized = False
|
|
if etudid in self.validations.ue_capitalisees.index:
|
|
ue_cap = self._get_etud_ue_cap(etudid, ue)
|
|
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
|
|
was_capitalized = True
|
|
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
|
|
moy_ue = ue_cap["moy_ue"]
|
|
is_capitalized = True
|
|
|
|
# Coef l'UE dans le semestre courant:
|
|
if self.is_apc:
|
|
# utilise les ECTS comme coef.
|
|
coef_ue = ue.ects
|
|
else:
|
|
# formations classiques
|
|
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
|
|
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
|
|
if self.is_apc:
|
|
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
|
|
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
|
|
coef_ue = ue_capitalized.ects
|
|
if coef_ue is None:
|
|
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
|
|
raise ScoValueError(
|
|
f"""L'UE capitalisée {ue_capitalized.acronyme}
|
|
du semestre {orig_sem.titre_annee()}
|
|
n'a pas d'indication d'ECTS.
|
|
Corrigez ou faite corriger le programme
|
|
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
|
|
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
|
|
"""
|
|
)
|
|
else:
|
|
# Coefs de l'UE capitalisée en formation classique:
|
|
# va chercher le coef dans le semestre d'origine
|
|
coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue(
|
|
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
|
|
)
|
|
|
|
return {
|
|
"is_capitalized": is_capitalized,
|
|
"was_capitalized": was_capitalized,
|
|
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
|
|
"coef_ue": coef_ue,
|
|
"ects_pot": ue.ects or 0.0,
|
|
"ects": self.validations.decisions_jury_ues.get(etudid, {})
|
|
.get(ue.id, {})
|
|
.get("ects", 0.0)
|
|
if self.validations.decisions_jury_ues
|
|
else 0.0,
|
|
"ects_ue": ue.ects,
|
|
"cur_moy_ue": cur_moy_ue,
|
|
"moy": moy_ue,
|
|
"event_date": ue_cap["event_date"] if is_capitalized else None,
|
|
"ue": ue.to_dict(),
|
|
"formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None,
|
|
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
|
|
}
|
|
|
|
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
|
|
"Détermine le coefficient de l'UE pour cet étudiant."
|
|
# calcul différent en classique et BUT
|
|
raise NotImplementedError()
|
|
|
|
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap): # UNUSED in ScoDoc 9
|
|
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
|
|
injectée dans le semestre courant.
|
|
|
|
ue : ue du semestre courant
|
|
|
|
ue_cap = resultat de formsemestre_get_etud_capitalisation
|
|
{ 'ue_id' (dans le semestre source),
|
|
'ue_code', 'moy', 'event_date','formsemestre_id' }
|
|
"""
|
|
# 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
|
|
ue_coef_db = FormSemestreUECoef.query.filter_by(
|
|
formsemestre_id=self.formsemestre.id, ue_id=ue.id
|
|
).first()
|
|
if ue_coef_db is not None:
|
|
return ue_coef_db.coefficient
|
|
|
|
# En APC: somme des coefs des modules vers cette UE
|
|
# En classique: Capitalisation UE externe: quel coef appliquer ?
|
|
# En ScoDoc 7, calculait la somme des coefs dans l'UE du semestre d'origine
|
|
# ici si l'étudiant est inscrit dans le semestre courant,
|
|
# somme des coefs des modules de l'UE auxquels il est inscrit
|
|
return self.compute_etud_ue_coef(etudid, ue)
|
|
|
|
# --- TABLEAU RECAP
|
|
|
|
def get_table_recap(
|
|
self,
|
|
convert_values=False,
|
|
include_evaluations=False,
|
|
mode_jury=False,
|
|
allow_html=True,
|
|
) -> tb.Table:
|
|
"""Table récap. des résultats.
|
|
allow_html: si vrai, peut mettre du HTML dans les valeurs
|
|
|
|
Result: XXX tuple avec
|
|
- rows: liste de dicts { column_id : value }
|
|
- titles: { column_id : title }
|
|
- columns_ids: (liste des id de colonnes)
|
|
|
|
Si convert_values, transforme les notes en chaines ("12.34").
|
|
Les colonnes générées sont:
|
|
etudid
|
|
rang : rang indicatif (basé sur moy gen)
|
|
moy_gen : moy gen indicative
|
|
moy_ue_<ue_id>, ..., les moyennes d'UE
|
|
moy_res_<modimpl_id>_<ue_id>, ... les moyennes de ressources dans l'UE
|
|
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
|
|
|
|
On ajoute aussi des attributs: XXX
|
|
- pour les lignes:
|
|
_css_row_class (inutilisé pour le monent)
|
|
_<column_id>_class classe css:
|
|
- la moyenne générale a la classe col_moy_gen
|
|
- les colonnes SAE ont la classe col_sae
|
|
- les colonnes Resources ont la classe col_res
|
|
- les colonnes d'UE ont la classe col_ue
|
|
- les colonnes de modules (SAE ou res.) d'une UE ont la classe mod_ue_<ue_id>
|
|
_<column_id>_order : clé de tri
|
|
"""
|
|
if convert_values:
|
|
fmt_note = scu.fmt_note
|
|
else:
|
|
fmt_note = lambda x: x
|
|
|
|
parcours = self.formsemestre.formation.get_parcours()
|
|
barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE
|
|
barre_valid_ue = parcours.NOTES_BARRE_VALID_UE
|
|
barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING
|
|
NO_NOTE = "-" # contenu des cellules sans notes
|
|
rows = []
|
|
dict_nom_res = {} # cache uid : nomcomplet
|
|
|
|
# def add_cell(
|
|
# row: dict,
|
|
# col_id: str,
|
|
# title: str,
|
|
# content: str,
|
|
# classes: str = "",
|
|
# idx: int = 100,
|
|
# ):
|
|
# "Add a cell to our table. classes is a list of css class names"
|
|
# row[col_id] = content
|
|
# if classes:
|
|
# row[f"_{col_id}_class"] = classes + f" c{idx}"
|
|
# if not col_id in titles:
|
|
# titles[col_id] = title
|
|
# titles[f"_{col_id}_col_order"] = idx
|
|
# if classes:
|
|
# titles[f"_{col_id}_class"] = classes
|
|
# return idx + 1
|
|
|
|
etuds_inscriptions = self.formsemestre.etuds_inscriptions
|
|
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
|
|
ues_sans_bonus = [ue for ue in ues if ue.type != UE_SPORT]
|
|
modimpl_ids = set() # modimpl effectivement présents dans la table
|
|
table = tb.Table()
|
|
for etudid in etuds_inscriptions:
|
|
idx = 0 # index de la colonne
|
|
etud = Identite.query.get(etudid)
|
|
row = tb.Row(table, etudid)
|
|
table.add_row(row)
|
|
# --- Codes (seront cachés, mais exportés en excel)
|
|
row.add_cell("etudid", "etudid", etudid, "codes")
|
|
row.add_cell(
|
|
"code_nip",
|
|
"code_nip",
|
|
etud.code_nip or "",
|
|
"codes",
|
|
)
|
|
|
|
# --- Rang
|
|
if not self.formsemestre.block_moyenne_generale:
|
|
row.add_cell(
|
|
"rang",
|
|
"Rg",
|
|
self.etud_moy_gen_ranks[etudid],
|
|
"rang",
|
|
data={"order": f"{self.etud_moy_gen_ranks_int[etudid]:05d}"},
|
|
)
|
|
# --- Identité étudiant
|
|
url_bulletin = url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=self.formsemestre.id,
|
|
etudid=etudid,
|
|
)
|
|
row.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
|
row.add_cell(
|
|
"nom_disp",
|
|
"Nom",
|
|
etud.nom_disp(),
|
|
"identite_detail",
|
|
data={"order": etud.sort_key},
|
|
target=url_bulletin,
|
|
target_attrs=f'class="etudinfo" id="{etudid}"',
|
|
)
|
|
row.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
|
row.add_cell(
|
|
"nom_short",
|
|
"Nom",
|
|
etud.nom_short,
|
|
"identite_court",
|
|
data={
|
|
"order": etud.sort_key,
|
|
"etudid": etud.id,
|
|
"nomprenom": etud.nomprenom,
|
|
},
|
|
target=url_bulletin,
|
|
target_attrs=f'class="etudinfo" id="{etudid}"',
|
|
)
|
|
|
|
# --- Moyenne générale
|
|
if not self.formsemestre.block_moyenne_generale:
|
|
moy_gen = self.etud_moy_gen.get(etudid, False)
|
|
note_class = ""
|
|
if moy_gen is False:
|
|
moy_gen = NO_NOTE
|
|
elif isinstance(moy_gen, float) and moy_gen < barre_moy:
|
|
note_class = "moy_ue_warning" # en rouge
|
|
row.add_cell(
|
|
"moy_gen",
|
|
"Moy",
|
|
fmt_note(moy_gen),
|
|
"col_moy_gen",
|
|
classes=[note_class],
|
|
)
|
|
# Ajoute bulle sur titre du pied de table:
|
|
table.foot_title_row.cells["moy_gen"].target_attrs = (
|
|
'title="moyenne indicative"' if self.is_apc else ""
|
|
)
|
|
# --- Moyenne d'UE
|
|
nb_ues_validables, nb_ues_warning = 0, 0
|
|
for ue in ues_sans_bonus:
|
|
ue_status = self.get_etud_ue_status(etudid, ue.id)
|
|
if ue_status is not None:
|
|
col_id = f"moy_ue_{ue.id}"
|
|
val = ue_status["moy"]
|
|
note_class = ""
|
|
if isinstance(val, float):
|
|
if val < barre_moy:
|
|
note_class = " moy_inf"
|
|
elif val >= barre_valid_ue:
|
|
note_class = " moy_ue_valid"
|
|
nb_ues_validables += 1
|
|
if val < barre_warning_ue:
|
|
note_class = " moy_ue_warning" # notes très basses
|
|
nb_ues_warning += 1
|
|
row.add_cell(
|
|
col_id,
|
|
ue.acronyme,
|
|
fmt_note(val),
|
|
group=f"col_ue_{ue.id}",
|
|
classes=["col_ue", note_class],
|
|
)
|
|
table.foot_title_row.cells[
|
|
col_id
|
|
].target_attrs = (
|
|
f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
|
)
|
|
if mode_jury:
|
|
# 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 = val_fmt_html = fmt_note(val)
|
|
if val:
|
|
val_fmt_html = f"""<span class="green-arrow-up"></span><span class="sp2l">{
|
|
val_fmt
|
|
}</span>"""
|
|
row.add_cell(
|
|
f"bonus_ue_{ue.id}",
|
|
f"Bonus {ue.acronyme}",
|
|
val_fmt_html if allow_html else val_fmt,
|
|
group=f"col_ue_{ue.id}",
|
|
classes=["col_ue_bonus"],
|
|
raw_content=val_fmt,
|
|
)
|
|
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
|
for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False):
|
|
if ue_status["is_capitalized"]:
|
|
val = "-c-"
|
|
else:
|
|
modimpl_results = self.modimpls_results.get(modimpl.id)
|
|
if modimpl_results: # pas bonus
|
|
if self.is_apc: # BUT
|
|
moys_vers_ue = modimpl_results.etuds_moy_module.get(
|
|
ue.id
|
|
)
|
|
val = (
|
|
moys_vers_ue.get(etudid, "?")
|
|
if moys_vers_ue is not None
|
|
else ""
|
|
)
|
|
else: # classique: Series indépendante de l'UE
|
|
val = modimpl_results.etuds_moy_module.get(
|
|
etudid, "?"
|
|
)
|
|
else:
|
|
val = ""
|
|
|
|
col_id = (
|
|
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
|
|
)
|
|
val_fmt = val_fmt_html = fmt_note(val)
|
|
if convert_values and (
|
|
modimpl.module.module_type == scu.ModuleType.MALUS
|
|
):
|
|
val_fmt_html = (
|
|
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
|
|
)
|
|
cell = row.add_cell(
|
|
col_id,
|
|
modimpl.module.code,
|
|
val_fmt_html,
|
|
group=f"col_ue_{ue.id}_modules",
|
|
classes=[
|
|
f"col_{modimpl.module.type_abbrv()}",
|
|
f"mod_ue_{ue.id}",
|
|
],
|
|
raw_content=val_fmt,
|
|
)
|
|
if modimpl.module.module_type == scu.ModuleType.MALUS:
|
|
# positionne la colonne à droite de l'UE
|
|
cell.group = f"col_ue_{ue.id}_malus"
|
|
table.insert_group(cell.group, after=f"col_ue_{ue.id}")
|
|
|
|
table.foot_title_row.cells[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
|
|
table.foot_title_row.cells[
|
|
col_id
|
|
].target_attrs = (
|
|
f""" title="{modimpl.module.titre} ({nom_resp})" """
|
|
)
|
|
modimpl_ids.add(modimpl.id)
|
|
nb_ues_etud_parcours = len(self.etud_ues_ids(etudid))
|
|
ue_valid_txt = (
|
|
ue_valid_txt_html
|
|
) = f"{nb_ues_validables}/{nb_ues_etud_parcours}"
|
|
if nb_ues_warning:
|
|
ue_valid_txt_html += " " + scu.EMO_WARNING
|
|
# place juste avant moy. gen.
|
|
table.insert_group("col_ues_validables", before="moy_gen")
|
|
classes = ["col_ue"]
|
|
if nb_ues_warning:
|
|
classes.append("moy_ue_warning")
|
|
elif nb_ues_validables < len(ues_sans_bonus):
|
|
classes.append("moy_inf")
|
|
row.add_cell(
|
|
"ues_validables",
|
|
"UEs",
|
|
ue_valid_txt_html,
|
|
"col_ues_validables",
|
|
classes=classes,
|
|
raw_content=ue_valid_txt,
|
|
data={"order": nb_ues_validables}, # tri
|
|
)
|
|
|
|
if mode_jury and self.validations:
|
|
if self.is_apc:
|
|
# formations BUT: pas de code semestre, concatene ceux des UEs
|
|
dec_ues = self.validations.decisions_jury_ues.get(etudid)
|
|
if dec_ues:
|
|
jury_code_sem = ",".join(
|
|
[dec_ues[ue_id].get("code", "") for ue_id in dec_ues]
|
|
)
|
|
else:
|
|
jury_code_sem = ""
|
|
else:
|
|
# formations classiques: code semestre
|
|
dec_sem = self.validations.decisions_jury.get(etudid)
|
|
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
|
row.add_cell("jury_code_sem", "Jury", jury_code_sem, "jury_code_sem")
|
|
row.add_cell(
|
|
"jury_link",
|
|
"",
|
|
f"""<a href="{url_for('notes.formsemestre_validation_etud_form',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre.id, etudid=etudid
|
|
)
|
|
}">{("saisir" if not jury_code_sem else "modifier")
|
|
if self.formsemestre.etat else "voir"} décisions</a>""",
|
|
"col_jury_link",
|
|
)
|
|
|
|
self.recap_add_partitions(table)
|
|
self.recap_add_cursus(table)
|
|
self._recap_add_admissions(table)
|
|
|
|
# tri par rang croissant
|
|
if not self.formsemestre.block_moyenne_generale:
|
|
table.sort_rows(key=lambda e: e["_rang_order"])
|
|
else:
|
|
table.sort_rows(key=lambda e: e["_ues_validables_order"], reverse=True)
|
|
|
|
# INFOS POUR FOOTER
|
|
bottom_infos = self._recap_bottom_infos(
|
|
table, ues_sans_bonus, modimpl_ids, fmt_note
|
|
)
|
|
if include_evaluations:
|
|
self._recap_add_evaluations(table)
|
|
|
|
# Ajoute style "col_empty" aux colonnes de modules vides
|
|
row_moy = table.get_row_by_id("moy")
|
|
for col_id in table.column_ids:
|
|
if "col_empty" in row_moy.cells[col_id]:
|
|
table.column_classes[col_id].append("col_empty")
|
|
|
|
# Ligne avec la classe de chaque colonne
|
|
# récupère le type à partir des classes css (hack...)
|
|
row_type = tb.BottomRow(
|
|
table,
|
|
"type_col",
|
|
title="Type col.",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
for col_id in table.column_ids:
|
|
group_name = table.column_group.get(col_id, "")
|
|
if group_name.startswith("col_"):
|
|
group_name = group_name[4:]
|
|
row_type.add_cell(col_id, None, group_name)
|
|
|
|
# Titres
|
|
table.add_head_row(self.head_title_row)
|
|
table.add_foot_row(self.foot_title_row)
|
|
return table
|
|
|
|
def _recap_bottom_infos(
|
|
self, table: tb.Table, ues, modimpl_ids: set, fmt_note
|
|
) -> dict:
|
|
"""Les informations à mettre en bas de la table: min, max, moy, ECTS, Apo"""
|
|
# Ordre des lignes: Min, Max, Moy, Coef, ECTS, Apo
|
|
row_min = tb.BottomRow(
|
|
table,
|
|
"min",
|
|
title="Min.",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
row_max = tb.BottomRow(
|
|
table,
|
|
"max",
|
|
title="Max.",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
row_moy = tb.BottomRow(
|
|
table,
|
|
"moy",
|
|
title="Moy.",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
row_coef = tb.BottomRow(
|
|
table,
|
|
"coef",
|
|
title="Coef.",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
row_ects = tb.BottomRow(
|
|
table,
|
|
"ects",
|
|
title="ECTS",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
row_apo = tb.BottomRow(
|
|
table,
|
|
"apo",
|
|
title="Code Apogée",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
|
|
# --- ECTS
|
|
# titre (à gauche) sur 2 colonnes pour s'adapter à l'affichage des noms/prenoms
|
|
for ue in ues:
|
|
col_id = f"moy_ue_{ue.id}"
|
|
row_ects.add_cell(col_id, None, ue.ects, "col_ue")
|
|
# ajoute cell UE vides sur ligne coef pour borders verticales
|
|
# XXX TODO classes dans table sur colonne ajoutées à tous les TD
|
|
row_coef.add_cell(col_id, None, "", "col_ue")
|
|
row_ects.add_cell(
|
|
"moy_gen",
|
|
None,
|
|
sum([ue.ects or 0 for ue in ues if ue.type != UE_SPORT]),
|
|
"col_moy_gen",
|
|
)
|
|
# --- MIN, MAX, MOY, APO
|
|
row_min.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.min()))
|
|
row_max.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.max()))
|
|
row_moy.add_cell("moy_gen", None, fmt_note(self.etud_moy_gen.mean()))
|
|
|
|
for ue in ues:
|
|
col_id = f"moy_ue_{ue.id}"
|
|
row_min.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].min()))
|
|
row_max.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].max()))
|
|
row_moy.add_cell(col_id, None, fmt_note(self.etud_moy_ue[ue.id].mean()))
|
|
row_apo.add_cell(col_id, None, 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}"
|
|
if self.is_apc:
|
|
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
|
|
else:
|
|
coef = modimpl.module.coefficient or 0
|
|
row_coef.add_cell(col_id, None, fmt_note(coef))
|
|
notes = self.modimpl_notes(modimpl.id, ue.id)
|
|
if np.isnan(notes).all():
|
|
# aucune note valide
|
|
row_min.add_cell(col_id, None, np.nan)
|
|
row_max.add_cell(col_id, None, np.nan)
|
|
moy = np.nan
|
|
else:
|
|
row_min.add_cell(col_id, None, fmt_note(np.nanmin(notes)))
|
|
row_max.add_cell(col_id, None, fmt_note(np.nanmax(notes)))
|
|
moy = np.nanmean(notes)
|
|
row_moy.add_cell(
|
|
col_id,
|
|
None,
|
|
fmt_note(moy),
|
|
# aucune note dans ce module ?
|
|
classes=["col_empty" if np.isnan(moy) else ""],
|
|
)
|
|
row_apo.add_cell(col_id, None, modimpl.module.code_apogee or "")
|
|
|
|
return { # { key : row } avec key = min, max, moy, coef, ...
|
|
"min": row_min,
|
|
"max": row_max,
|
|
"moy": row_moy,
|
|
"coef": row_coef,
|
|
"ects": row_ects,
|
|
"apo": row_apo,
|
|
}
|
|
|
|
def _recap_etud_groups_infos(
|
|
self, etudid: int, row: dict, titles: dict
|
|
): # XXX non utilisé
|
|
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
|
|
# dec = self.get_etud_decision_sem(etudid)
|
|
# if dec:
|
|
# codes_nb[dec["code"]] += 1
|
|
row_class = ""
|
|
etud_etat = self.get_etud_etat(etudid)
|
|
if etud_etat == DEM:
|
|
gr_name = "Dém."
|
|
row_class = "dem"
|
|
elif etud_etat == DEF:
|
|
gr_name = "Déf."
|
|
row_class = "def"
|
|
else:
|
|
# XXX probablement à revoir pour utiliser données cachées,
|
|
# via get_etud_groups_in_partition ou autre
|
|
group = sco_groups.get_etud_main_group(etudid, self.formsemestre.id)
|
|
gr_name = group["group_name"] or ""
|
|
row["group"] = gr_name
|
|
row["_group_class"] = "group"
|
|
if row_class:
|
|
row["_tr_class"] = " ".join([row.get("_tr_class", ""), row_class])
|
|
titles["group"] = "Gr"
|
|
|
|
def _recap_add_admissions(self, table: tb.Table):
|
|
"""Ajoute les colonnes "admission"
|
|
Les colonnes ont la classe css "admission"
|
|
"""
|
|
fields = {
|
|
"bac": "Bac",
|
|
"specialite": "Spécialité",
|
|
"type_admission": "Type Adm.",
|
|
"classement": "Rg. Adm.",
|
|
}
|
|
first = True
|
|
for cid, title in fields.items():
|
|
cell_head, cell_foot = table.add_title(cid, title)
|
|
cell_head.classes.append("admission")
|
|
cell_foot.classes.append("admission")
|
|
if first:
|
|
cell_head.classes.append("admission_first")
|
|
cell_foot.classes.append("admission_first")
|
|
first = False
|
|
|
|
for row in table.rows:
|
|
etud = Identite.query.get(row.id) # TODO XXX
|
|
admission = etud.admission.first()
|
|
first = True
|
|
for cid, title in fields.items():
|
|
cell = row.add_cell(
|
|
cid,
|
|
title,
|
|
getattr(admission, cid) or "",
|
|
"admission",
|
|
)
|
|
if first:
|
|
cell.classes.append("admission_first")
|
|
first = False
|
|
|
|
def recap_add_cursus(self, table: tb.Table):
|
|
"""Ajoute colonne avec code cursus, eg 'S1 S2 S1'"""
|
|
table.insert_group("cursus", before="col_ues_validables")
|
|
cid = "code_cursus"
|
|
formation_code = self.formsemestre.formation.formation_code
|
|
for row in table.rows:
|
|
etud = Identite.query.get(row.id) # TODO XXX à optimiser: etud dans row
|
|
row.add_cell(
|
|
cid,
|
|
"Cursus",
|
|
" ".join(
|
|
[
|
|
f"S{ins.formsemestre.semestre_id}"
|
|
for ins in reversed(etud.inscriptions())
|
|
if ins.formsemestre.formation.formation_code == formation_code
|
|
]
|
|
),
|
|
"cursus",
|
|
)
|
|
|
|
def recap_add_partitions(self, table: tb.Table):
|
|
"""Ajoute les colonnes indiquant les groupes
|
|
La table contient des rows avec la clé etudid.
|
|
|
|
Les colonnes ont la classe css "partition"
|
|
"""
|
|
table.insert_group("partition", after="parcours")
|
|
partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
|
|
self.formsemestre.id
|
|
)
|
|
first_partition = True
|
|
for partition in partitions:
|
|
col_classes = ["partition"]
|
|
if not first_partition:
|
|
col_classes.append("partition_aux")
|
|
first_partition = False
|
|
cid = f"part_{partition['partition_id']}"
|
|
cell_head, cell_foot = table.add_title(cid, partition["partition_name"])
|
|
cell_head.classes += col_classes
|
|
cell_foot.classes += col_classes
|
|
|
|
if partition["bul_show_rank"]:
|
|
rg_cid = cid + "_rg" # rang dans la partition
|
|
cell_head, cell_foot = table.add_title(
|
|
cid, f"Rg {partition['partition_name']}"
|
|
)
|
|
cell_head.classes.append("partition_rangs")
|
|
cell_foot.classes.append("partition_rangs")
|
|
|
|
partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
|
|
for row in table.rows:
|
|
group = None # group (dict) de l'étudiant dans cette partition
|
|
# dans NotesTableCompat, à revoir
|
|
etud_etat = self.get_etud_etat(row.id) # row.id == etudid
|
|
tr_classes = []
|
|
if etud_etat == scu.DEMISSION:
|
|
gr_name = "Dém."
|
|
tr_classes.append("dem")
|
|
elif etud_etat == DEF:
|
|
gr_name = "Déf."
|
|
tr_classes.append("def")
|
|
else:
|
|
group = partition_etud_groups.get(row["etudid"])
|
|
gr_name = group["group_name"] if group else ""
|
|
if gr_name:
|
|
row.add_cell(
|
|
cid,
|
|
partition["partition_name"],
|
|
gr_name,
|
|
"partition",
|
|
classes=col_classes,
|
|
)
|
|
|
|
# Rangs dans groupe
|
|
if (
|
|
partition["bul_show_rank"]
|
|
and (group is not None)
|
|
and (group["id"] in self.moy_gen_rangs_by_group)
|
|
):
|
|
rang = self.moy_gen_rangs_by_group[group["id"]][0]
|
|
row.add_cell(rg_cid, None, rang.get(row["etudid"], ""), "partition")
|
|
|
|
def _recap_add_evaluations(self, table: tb.Table):
|
|
"""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:
|
|
row_descr_eval = tb.BottomRow(
|
|
table,
|
|
"evaluations",
|
|
title="Description évaluations",
|
|
left_title_col_ids=["prenom", "nom_short"],
|
|
category="bottom_infos",
|
|
classes=["bottom_info"],
|
|
)
|
|
|
|
first_eval = True
|
|
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:
|
|
col_id = f"eval_{e.id}"
|
|
title = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
|
|
col_classes = ["evaluation"]
|
|
if first_eval:
|
|
col_classes.append("first")
|
|
elif first_eval_of_mod:
|
|
col_classes.append("first_of_mod")
|
|
first_eval_of_mod = first_eval = False
|
|
eval_index -= 1
|
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
|
e.evaluation_id
|
|
)
|
|
for row in table.rows:
|
|
etudid = row.id
|
|
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
|
|
content = scu.fmt_note(val)
|
|
classes = col_classes + [
|
|
{
|
|
"ABS": "abs",
|
|
"ATT": "att",
|
|
"EXC": "exc",
|
|
}.get(content, "")
|
|
]
|
|
row.add_cell(col_id, title, content, "", classes=classes)
|
|
else:
|
|
row.add_cell(
|
|
col_id,
|
|
title,
|
|
"ni",
|
|
"",
|
|
classes=col_classes + ["non_inscrit"],
|
|
)
|
|
|
|
table.get_row_by_id("coef").row[col_id] = e.coefficient
|
|
table.get_row_by_id("min").row[col_id] = "0"
|
|
table.get_row_by_id("max").row[col_id] = scu.fmt_note(e.note_max)
|
|
row_descr_eval.add_cell(
|
|
col_id,
|
|
None,
|
|
e.description or "",
|
|
target=url_for(
|
|
"notes.evaluation_listenotes",
|
|
scodoc_dept=g.scodoc_dept,
|
|
evaluation_id=e.id,
|
|
),
|
|
)
|