WIP: calcul unifié, bonus sport BUT

This commit is contained in:
Emmanuel Viennet 2022-01-25 10:45:13 +01:00
parent 2b1a3ee95e
commit 8385941cf6
34 changed files with 757 additions and 156 deletions

View File

@ -13,6 +13,7 @@ from flask import url_for, g
from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import fmt_note
from app.comp.res_but import ResultatsSemestreBUT
@ -49,23 +50,30 @@ class BulletinBUT(ResultatsSemestreBUT):
d = {
"id": ue.id,
"numero": ue.numero,
"type": ue.type,
"ECTS": {
"acquis": 0, # XXX TODO voir jury
"total": ue.ects,
},
"color": ue.color,
"competence": None, # XXX TODO lien avec référentiel
"moyenne": {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
},
"bonus": None, # XXX TODO
"moyenne": None,
# Le bonus sport appliqué sur cette UE
"bonus": self.bonus_ues[ue.id][etud.id]
if self.bonus_ues is not None and ue.id in self.bonus_ues
else 0.0,
"malus": None, # XXX TODO voir ce qui est ici
"capitalise": None, # "AAAA-MM-JJ" TODO
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
}
if ue.type != UE_SPORT:
d["moyenne"] = {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
}
return d
def etud_mods_results(self, etud, modimpls) -> dict:

View File

@ -134,8 +134,12 @@ def bulletin_but_xml_compat(
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
)
)
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
bonus = 0 # XXX TODO valeur du bonus sport
rang = 0 # XXX TODO rang de l'étudiant selon la moy gen indicative
# valeur du bonus sport
if results.bonus is not None:
bonus = results.bonus[etud.id]
else:
bonus = 0
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
doc.append(Element("note_max", value="20")) # notes toujours sur 20

View File

@ -19,12 +19,16 @@ class StatsMoyenne:
def __init__(self, vals):
"""Calcul les statistiques.
Les valeurs NAN ou non numériques sont toujours enlevées.
Si vals is None, renvoie des zéros (utilisé pour UE bonus)
"""
self.moy = np.nanmean(vals)
self.min = np.nanmin(vals)
self.max = np.nanmax(vals)
self.size = len(vals)
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
if vals is None:
self.moy = self.min = self.max = self.size = self.nb_vals = 0
else:
self.moy = np.nanmean(vals)
self.min = np.nanmin(vals)
self.max = np.nanmax(vals)
self.size = len(vals)
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
def to_dict(self):
"Tous les attributs dans un dict"

322
app/comp/bonus_spo.py Normal file
View File

@ -0,0 +1,322 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Classes spécifiques de calcul du bonus sport, culture ou autres activités
Les classes de Bonus fournissent deux méthodes:
- get_bonus_ues()
- get_bonus_moy_gen()
"""
import numpy as np
import pandas as pd
from app import db
from app import models
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import bonus_sport
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
def get_bonus_sport_class_from_name(dept_id):
"""La classe de bonus sport pour le département indiqué.
Note: en ScoDoc 9, le bonus sport est défini gloabelement et
ne dépend donc pas du département.
Résultat: une sous-classe de BonusSport
"""
raise NotImplementedError()
class BonusSport:
"""Calcul du bonus sport.
Arguments:
- sem_modimpl_moys :
notes moyennes aux modules (tous les étuds x tous les modimpls)
floats avec des NaN.
En classique: sem_matrix, ndarray (etuds x modimpls)
En APC: sem_cube, ndarray (etuds x modimpls x UEs)
- ues: les ues du semestre (incluant le bonus sport)
- modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
- modimpl_coefs: les coefs des modules
En classique: 1d ndarray de float (modimpl)
En APC: 2d ndarray de float, (modimpl x UE) <= attention à transposer
"""
# Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen:
apc_apply_bonus_mg_to_ues = True
# Attributs virtuels:
seuil_moy_gen = None
proportion_point = None
bonus_moy_gen_limit = None
name = "virtual"
def __init__(
self,
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
):
self.formsemestre = formsemestre
self.ues = ues
self.etuds_idx = modimpl_inscr_df.index # les étudiants inscrits au semestre
self.bonus_ues: pd.DataFrame = None # virtual
self.bonus_moy_gen: pd.Series = None # virtual
# Restreint aux modules standards des UE de type "sport":
modimpl_mask = np.array(
[
(m.module.module_type == ModuleType.STANDARD)
and (m.module.ue.type == UE_SPORT)
for m in formsemestre.modimpls_sorted
]
)
self.modimpls_spo = [
modimpl
for i, modimpl in enumerate(formsemestre.modimpls_sorted)
if modimpl_mask[i]
]
"liste des modimpls sport"
# Les moyennes des modules "sport": (une par UE en APC)
sem_modimpl_moys_spo = sem_modimpl_moys[:, modimpl_mask]
# Les inscriptions aux modules sport:
modimpl_inscr_spo = modimpl_inscr_df.values[:, modimpl_mask]
# Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue)
modimpl_coefs_spo = modimpl_coefs[modimpl_mask]
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
# ou (nb_etuds, nb_mod_sport, nb_ues)
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
nb_ues = len(ues)
# Enlève les NaN du numérateur:
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
if formsemestre.formation.is_apc():
# BUT
nb_ues_no_bonus = sem_modimpl_moys.shape[2]
# Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_spo_stacked = np.stack(
[modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_modimpl_moys_inscrits = np.where(
modimpl_inscr_spo_stacked, sem_modimpl_moys_no_nan, 0.0
)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr_spo_stacked,
np.stack([modimpl_coefs_spo.T] * nb_etuds),
0.0,
)
else:
# Formations classiques
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_modimpl_moys_inscrits = np.where(
modimpl_inscr_spo, sem_modimpl_moys_no_nan, 0.0
)
modimpl_coefs_spo = modimpl_coefs_spo.T
modimpl_coefs_etuds = np.where(
modimpl_inscr_spo, np.stack([modimpl_coefs_spo] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN (nb_etuds x nb_mod_sport)
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_modimpl_moys_spo), 0.0, modimpl_coefs_etuds
)
#
self.compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
def compute_bonus(
self,
sem_modimpl_moys_inscrits: np.ndarray,
modimpl_coefs_etuds_no_nan: np.ndarray,
):
"""Calcul des bonus: méthode virtuelle à écraser.
Arguments:
- sem_modimpl_moys_inscrits:
ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue)
les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
- modimpl_coefs_etuds_no_nan:
les coefficients: float ndarray
Résultat: None
"""
raise NotImplementedError("méthode virtuelle")
def get_bonus_ues(self) -> pd.Series:
"""Les bonus à appliquer aux UE
Résultat: DataFrame de float, index etudid, columns: ue.id
"""
if (
self.formsemestre.formation.is_apc()
and self.apc_apply_bonus_mg_to_ues
and self.bonus_ues is None
):
# reporte uniformément le bonus moyenne générale sur les UEs
# (assure la compatibilité de la plupart des anciens bonus avec le BUT)
# ues = self.formsemestre.query_ues(with_sport=False)
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
bonus_moy_gen = self.get_bonus_moy_gen()
bonus_ues = np.stack([bonus_moy_gen.values] * len(ues_idx), axis=1)
return pd.DataFrame(bonus_ues, index=self.etuds_idx, columns=ues_idx)
return self.bonus_ues
def get_bonus_moy_gen(self):
"""Le bonus à appliquer à la moyenne générale.
Résultat: Series de float, index etudid
"""
return self.bonus_moy_gen
class BonusSportSimples(BonusSport):
"""Les bonus sport simples calcule un bonus à partir des notes moyennes de modules
de l'UE sport, et ce bonus est soit appliqué sur la moyenne générale (formations classiques),
soit réparti sur les UE (formations APC).
Le bonus est par défaut calculé comme:
Les points au-dessus du seuil (par défaut) 10 sur 20 obtenus dans chacun des
modules optionnels sont cumulés et une fraction de ces points cumulés s'ajoute
à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
bonus_moy_gen_arr = np.sum(
np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
* self.proportion_point,
0.0,
),
axis=1,
)
# en APC, applati la moyenne gen. XXX pourrait être fait en amont
if len(bonus_moy_gen_arr.shape) > 1:
bonus_moy_gen_arr = bonus_moy_gen_arr.sum(axis=1)
# Bonus moyenne générale, et 0 sur les UE
self.bonus_moy_gen = pd.Series(
bonus_moy_gen_arr, index=self.etuds_idx, dtype=float
)
if self.bonus_moy_gen_limit is not None:
# Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit)
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
# bonus_ue = np.stack([modimpl_coefs_spo.T] * nb_ues)
class BonusIUTV(BonusSportSimples):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 13 (sports, musique, deuxième langue,
culture, etc) non rattachés à une unité d'enseignement. Les points
au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
name = "bonus_iutv"
pass # oui, c'ets le bonus par défaut
class BonusDirect(BonusSportSimples):
"""Bonus direct: les points sont directement ajoutés à la moyenne générale.
Les coefficients sont ignorés: tous les points de bonus sont sommés.
(rappel: la note est ramenée sur 20 avant application).
"""
name = "bonus_direct"
seuil_moy_gen = 0.0 # seuls le spoints au dessus du seuil sont comptés
proportion_point = 1.0
class BonusIUTStDenis(BonusIUTV):
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points."""
name = "bonus_iut_stdenis"
bonus_moy_gen_limit = 0.5
class BonusColmar(BonusSportSimples):
"""Calcul bonus modules optionels (sport, culture), règle IUT Colmar.
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non
rattachés à une unité d'enseignement. Les points au-dessus de 10
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
# note: cela revient à dire que l'on ajoute 5% des points au dessus de 10,
# et qu'on limite à 5% de 10, soit 0.5 points
# ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis)
name = "bonus_colmar"
bonus_moy_gen_limit = 0.5
class BonusVilleAvray:
"""Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
Si la note est >= 10 et < 12, bonus de 0.1 point
Si la note est >= 12 et < 16, bonus de 0.2 point
Si la note est >= 16, bonus de 0.3 point
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant.
"""
name = "bonus_iutva"
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Calcule moyenne pondérée des notes de sport:
bonus_moy_gen_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus_moy_gen_arr[bonus_moy_gen_arr >= 10.0] = 0.1
bonus_moy_gen_arr[bonus_moy_gen_arr >= 12.0] = 0.2
bonus_moy_gen_arr[bonus_moy_gen_arr >= 16.0] = 0.3
# Bonus moyenne générale, et 0 sur les UE
self.bonus_moy_gen = pd.Series(
bonus_moy_gen_arr, index=self.etuds_idx, dtype=float
)
if self.bonus_moy_gen_limit is not None:
# Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit)
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
def get_bonus_class_dict(start=BonusSport, d=None):
"""Dictionnaire des classes de bonus
(liste les sous-classes de BonusSport ayant un nom)
Resultat: { name : class }
"""
if d is None:
d = {}
if start.name != "virtual":
d[start.name] = start
for subclass in start.__subclasses__():
get_bonus_class_dict(subclass, d=d)
return d

View File

@ -21,7 +21,7 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
value: bool (0/1 inscrit ou pas)
"""
# méthode la moins lente: une requete par module, merge les dataframes
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
etudids = [inscr.etudid for inscr in formsemestre.inscriptions]
df = pd.DataFrame(index=etudids, dtype=int)
for moduleimpl_id in moduleimpl_ids:
@ -47,10 +47,10 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
def df_load_modimpl_inscr_v0(formsemestre):
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
etudids = [i.etudid for i in formsemestre.inscriptions]
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
for modimpl in formsemestre.modimpls:
for modimpl in formsemestre.modimpls_sorted:
ins_mod = df[modimpl.id]
for inscr in modimpl.inscriptions:
ins_mod[inscr.etudid] = True
@ -58,7 +58,7 @@ def df_load_modimpl_inscr_v0(formsemestre):
def df_load_modimpl_inscr_v2(formsemestre):
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
etudids = [i.etudid for i in formsemestre.inscriptions]
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
cursor = db.engine.execute(

View File

@ -65,8 +65,9 @@ class ModuleImplResults:
self.module_id = moduleimpl.module.id
self.etudids = None
"liste des étudiants inscrits au SEMESTRE"
self.nb_inscrits_module = None
"nombre d'inscrits (non DEM) au module"
"nombre d'inscrits (non DEM) à ce module"
self.evaluations_completes = []
"séquence de booléens, indiquant les évals à prendre en compte."
self.evaluations_completes_dict = {}
@ -263,14 +264,12 @@ class ModuleImplResultsAPC(ModuleImplResults):
return self.etuds_moy_module
def load_evaluations_poids(
moduleimpl_id: int, default_poids=1.0
) -> tuple[pd.DataFrame, list]:
def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
"""Charge poids des évaluations d'un module et retourne un dataframe
rows = evaluations, columns = UE, value = poids (float).
Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies par default_poids.
Résultat: (evals_poids, liste de UE du semestre)
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon.
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
"""
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
@ -282,8 +281,14 @@ def load_evaluations_poids(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
if default_poids is not None:
evals_poids.fillna(value=default_poids, inplace=True)
# Initialise poids non enregistrés:
if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict()
for ue in ues:
evals_poids[ue.id][evals_poids[ue.id].isna()] = (
1 if ue_coefs.get(ue.id, 0.0) > 0 else 0
)
return evals_poids, ues
@ -296,6 +301,7 @@ def moduleimpl_is_conforme(
évaluations vers une UE de coefficient non nul est non nulle.
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
NB: les UEs dans evals_poids sont sans le bonus sport
"""
nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0:

View File

@ -38,7 +38,7 @@ def compute_sem_moys_apc(
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE (sans ue bonus)
Result: panda Series, index etudid, valeur float (moyenne générale)
"""

View File

@ -62,6 +62,10 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
.filter(
(Module.module_type == ModuleType.RESSOURCE)
| (Module.module_type == ModuleType.SAE)
| (
(Module.ue_id == UniteEns.id)
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
)
)
.order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
@ -102,13 +106,13 @@ def df_load_modimpl_coefs(
et modules du formsemestre.
Si ues et modimpls sont None, prend tous ceux du formsemestre.
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modimpl, value = coef.
DataFrame rows = UEs (avec bonus), columns = modimpl, value = coef.
"""
if ues is None:
ues = formsemestre.query_ues().all()
ue_ids = [x.id for x in ues]
if modimpls is None:
modimpls = formsemestre.modimpls.all()
modimpls = formsemestre.modimpls_sorted
modimpl_ids = [x.id for x in modimpls]
mod2impl = {m.module.id: m.id for m in modimpls}
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
@ -134,7 +138,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
assert len(modimpls_notes)
modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE)
# passe de (mod x etud x ue) à (etud x mod x ue)
return modimpls_notes.swapaxes(0, 1)
@ -144,10 +148,14 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
et assemble le cube.
etuds: tous les inscrits au semestre (avec dem. et def.)
modimpls: _tous_ les modimpls de ce semestre
UEs: X?X voir quelles sont les UE considérées ici
modimpls: _tous_ les modimpls de ce semestre (y compris bonus sport)
UEs: toutes les UE du semestre (même si pas d'inscrits) SAUF le sport.
Resultat:
Attention: la liste des modimpls inclut les modules des UE sport, mais
elles ne sont pas dans la troisième dimension car elles n'ont pas de
"moyenne d'UE".
Résultat:
sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids }
modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
@ -155,7 +163,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
modimpls_results = {}
modimpls_evals_poids = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
@ -194,26 +202,27 @@ def compute_ue_moys_apc(
modimpls : liste des modules à considérer (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
Resultat: DataFrame columns UE, rows etudid
Résultat: DataFrame columns UE (sans sport), rows etudid
"""
nb_etuds, nb_modules, nb_ues = sem_cube.shape
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
nb_ues_tot = len(ues)
assert len(modimpls) == nb_modules
if nb_modules == 0 or nb_etuds == 0:
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
assert len(etuds) == nb_etuds
assert len(ues) == nb_ues
assert modimpl_inscr_df.shape[0] == nb_etuds
assert modimpl_inscr_df.shape[1] == nb_modules
assert modimpl_coefs_df.shape[0] == nb_ues
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values
# Duplique les inscriptions sur les UEs:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
# Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
# Enlève les NaN du numérateur:
# si on veut prendre en compte les modules avec notes neutralisées ?
sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)
@ -234,7 +243,9 @@ def compute_ue_moys_apc(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
return pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
etud_moy_ue,
index=modimpl_inscr_df.index, # les etudids
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
)
@ -244,6 +255,7 @@ def compute_ue_moys_classic(
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique.
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
@ -251,13 +263,19 @@ def compute_ue_moys_classic(
NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
sem_matrix: notes moyennes aux modules
L'éventuel bonus sport n'est PAS appliqué ici.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (SAEs, tout sauf sport, ...).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
ues : liste des UE
ues : liste des UE du semestre
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
modimpl_mask: masque des modimpls à prendre en compte
Résultat:
- moyennes générales: pd.Series, index etudid
@ -266,10 +284,15 @@ def compute_ue_moys_classic(
les coefficients effectifs de chaque UE pour chaque étudiant
(sommes de coefs de modules pris en compte)
"""
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
nb_ues = len(ues)
modimpl_inscr = modimpl_inscr_df.values
# Enlève les NaN du numérateur:
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
@ -291,8 +314,8 @@ def compute_ue_moys_classic(
etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index)
# Calcul des moyennes d'UE
ue_modules = np.array(
[[m.module.ue == ue for m in formsemestre.modimpls] for ue in ues]
)[..., np.newaxis]
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
)[..., np.newaxis][:, modimpl_mask, :]
modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues
)

View File

@ -10,6 +10,9 @@ import pandas as pd
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.scodoc.sco_codes_parcours import UE_SPORT
class ResultatsSemestreBUT(NotesTableCompat):
@ -37,26 +40,44 @@ class ResultatsSemestreBUT(NotesTableCompat):
) = moy_ue.notes_sem_load_cube(self.formsemestre)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
self.formsemestre, ues=self.ues, modimpls=self.modimpls
self.formsemestre, ues=self.ues, modimpls=self.formsemestre.modimpls_sorted
)
# l'idx de la colonne du mod modimpl.id est
# modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
# Elimine les coefs des UE bonus sports
no_bonus = [ue.type != UE_SPORT for ue in self.ues]
modimpl_coefs_no_bonus_df = self.modimpl_coefs_df[no_bonus]
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.modimpls,
self.formsemestre.modimpls_sorted,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
modimpl_coefs_no_bonus_df,
)
# Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame(
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
self.etud_moy_ue, modimpl_coefs_no_bonus_df
)
# --- Bonus Sport & Culture
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:
bonus: BonusSport = bonus_class(
self.formsemestre,
self.sem_cube,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df.transpose(),
)
self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None:
self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:

View File

@ -11,7 +11,11 @@ import pandas as pd
from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
class ResultatsSemestreClassic(NotesTableCompat):
@ -41,11 +45,20 @@ class ResultatsSemestreClassic(NotesTableCompat):
)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs = np.array(
[m.module.coefficient for m in self.formsemestre.modimpls]
[m.module.coefficient for m in self.formsemestre.modimpls_sorted]
)
self.modimpl_idx = {m.id: i for i, m in enumerate(self.formsemestre.modimpls)}
self.modimpl_idx = {
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
}
"l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]"
modimpl_standards_mask = np.array(
[
(m.module.module_type == ModuleType.STANDARD)
and (m.module.ue.type != UE_SPORT)
for m in self.formsemestre.modimpls_sorted
]
)
(
self.etud_moy_gen,
self.etud_moy_ue,
@ -56,7 +69,28 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
modimpl_standards_mask,
)
# --- Bonus Sport & Culture
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:
bonus: BonusSport = bonus_class(
self.formsemestre,
self.sem_matrix,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
)
self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None:
self.etud_moy_ue += self.bonus_ues # somme les dataframes
bonus_mg = bonus.get_bonus_moy_gen()
if bonus_mg is not None:
self.etud_moy_gen += bonus_mg
self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
)
# --- Classements:
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
@ -85,9 +119,9 @@ class ResultatsSemestreClassic(NotesTableCompat):
}
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple:
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
"""Calcule la matrice des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
(charge toutes les notes, calcule les moyennes des modules
et assemble la matrice)
Resultat:
sem_matrix : 2d-array (etuds x modimpls)
@ -95,7 +129,7 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple:
"""
modimpls_results = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
etuds_moy_module = mod_results.compute_module_moy()
modimpls_results[modimpl.id] = mod_results

View File

@ -100,42 +100,28 @@ class ResultatsSemestre:
@cached_property
def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre
"""Liste des UEs du semestre (avec les UE bonus sport)
(indices des DataFrames)
"""
return self.formsemestre.query_ues(with_sport=True).all()
@cached_property
def modimpls(self):
"""Liste des modimpls du semestre
- triée par numéro de module en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.formsemestre.modimpls.all()
if self.is_apc:
modimpls.sort(key=lambda m: (m.module.numero, m.module.code))
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls
@cached_property
def ressources(self):
"Liste des ressources du semestre, triées par numéro de module"
return [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
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.modimpls if m.module.module_type == scu.ModuleType.SAE]
return [
m
for m in self.formsemestre.modimpls_sorted
if m.module.module_type == scu.ModuleType.SAE
]
@cached_property
def ue_validables(self) -> list:
@ -163,16 +149,20 @@ class NotesTableCompat(ResultatsSemestre):
développements (API malcommode et peu efficace).
"""
_cached_attrs = ResultatsSemestre._cached_attrs + ()
_cached_attrs = ResultatsSemestre._cached_attrs + (
"bonus",
"bonus_ues",
)
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre)
nb_etuds = len(self.etuds)
self.bonus = defaultdict(lambda: 0.0) # XXX TODO
self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues}
self.bonus = None # virtuel
self.bonus_ues = None # virtuel
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
self.mod_rangs = {
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls
m.id: (None, nb_etuds) for m in self.formsemestre.modimpls_sorted
}
self.moy_min = "NA"
self.moy_max = "NA"
@ -221,7 +211,11 @@ class NotesTableCompat(ResultatsSemestre):
ues = []
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
d = ue.to_dict()
d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).to_dict())
if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id]
else:
moys = None
d.update(StatsMoyenne(moys).to_dict())
ues.append(d)
return ues
@ -230,9 +224,13 @@ class NotesTableCompat(ResultatsSemestre):
triés par numéros (selon le type de formation)
"""
if ue_id is None:
return [m.to_dict() for m in self.modimpls]
return [m.to_dict() for m in self.formsemestre.modimpls_sorted]
else:
return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id]
return [
m.to_dict()
for m in self.formsemestre.modimpls_sorted
if m.module.ue.id == ue_id
]
def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
@ -359,12 +357,16 @@ class NotesTableCompat(ResultatsSemestre):
moy_gen = self.etud_moy_gen.get(etudid, False)
if moy_gen is False:
# pas de moyenne: démissionnaire ou def
t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls)
t = (
["-"]
+ ["0.00"] * len(self.ues)
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
)
else:
moy_ues = self.etud_moy_ue.loc[etudid]
t = [moy_gen] + list(moy_ues)
# TODO UE capitalisées: ne pas afficher moyennes modules
for modimpl in self.modimpls:
for modimpl in self.formsemestre.modimpls_sorted:
val = self.get_etud_mod_moy(modimpl.id, etudid)
t.append(val)
t.append(etudid)

View File

@ -310,7 +310,7 @@ class ScoDocConfigurationForm(FlaskForm):
label="Fonction de calcul des bonus sport&culture",
choices=[
(x, x if x else "Aucune")
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
for x in ScoDocSiteConfig.get_bonus_sport_class_names()
],
)
depts = FieldList(FormField(DeptForm))
@ -363,7 +363,7 @@ class ScoDocConfigurationForm(FlaskForm):
def select_action(self):
if (
self.data["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
!= ScoDocSiteConfig.get_bonus_sport_class_name()
):
return BonusSportUpdate(self.data)
for dept_entry in self.depts:
@ -381,7 +381,7 @@ def configuration():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
form = ScoDocConfigurationForm(
data=_make_data(
bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(),
bonus_sport=ScoDocSiteConfig.get_bonus_sport_class_name(),
modele=sco_logos.list_logos(),
)
)

View File

@ -112,6 +112,9 @@ class FormSemestre(db.Model):
if self.modalite is None:
self.modalite = FormationModalite.DEFAULT_MODALITE
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
def to_dict(self):
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
@ -152,6 +155,28 @@ class FormSemestre(db.Model):
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
return sem_ues.order_by(UniteEns.numero)
@cached_property
def modimpls_sorted(self) -> list[ModuleImpl]:
"""Liste des modimpls du semestre
- triée par type/numéro/code en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.modimpls.all()
if self.formation.is_apc():
modimpls.sort(
key=lambda m: (m.module.module_type, m.module.numero, m.module.code)
)
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls
def est_courant(self) -> bool:
"""Vrai si la date actuelle (now) est dans le semestre
(les dates de début et fin sont incluses)

View File

@ -5,7 +5,7 @@ import pandas as pd
from app import db
from app.comp import df_cache
from app.models import UniteEns, Identite
from app.models import Identite, Module
import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu
@ -127,3 +127,16 @@ class ModuleImplInscription(db.Model):
ModuleImpl,
backref=db.backref("inscriptions", cascade="all, delete-orphan"),
)
@classmethod
def nb_inscriptions_dans_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id,
Module.ue_id == ue_id,
).count()

View File

@ -4,6 +4,7 @@
from app import db
from app.models import APO_CODE_STR_LEN
from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
@ -131,7 +132,8 @@ class Module(db.Model):
def ue_coefs_list(self, include_zeros=True):
"""Liste des coefs vers les UE (pour les modules APC).
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre.
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
sauf UE bonus sport.
Result: List of tuples [ (ue, coef) ]
"""
if not self.is_apc():
@ -140,6 +142,7 @@ class Module(db.Model):
# Toutes les UE du même semestre:
ues_semestre = (
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
.filter(UniteEns.type != UE_SPORT)
.order_by(UniteEns.numero)
.all()
)

View File

@ -3,7 +3,7 @@
"""Model : preferences
"""
from app import db, log
from app.scodoc import bonus_sport
from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
@ -61,47 +61,80 @@ class ScoDocSiteConfig(db.Model):
}
@classmethod
def set_bonus_sport_func(cls, func_name):
def set_bonus_sport_class(cls, class_name):
"""Record bonus_sport config.
If func_name not defined, raise NameError
If class_name not defined, raise NameError
"""
if func_name not in cls.get_bonus_sport_func_names():
raise NameError("invalid function name for bonus_sport")
if class_name not in cls.get_bonus_sport_class_names():
raise NameError("invalid class name for bonus_sport")
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c:
log("setting to " + func_name)
c.value = func_name
log("setting to " + class_name)
c.value = class_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name)
c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_func_name(cls):
def get_bonus_sport_class_name(cls):
"""Get configured bonus function name, or None if None."""
f = cls.get_bonus_sport_func_from_name()
if f is None:
klass = cls.get_bonus_sport_class_from_name()
if klass is None:
return ""
else:
return f.__name__
return klass.name
@classmethod
def get_bonus_sport_class(cls):
"""Get configured bonus function, or None if None."""
return cls.get_bonus_sport_class_from_name()
@classmethod
def get_bonus_sport_class_from_name(cls, class_name=None):
"""returns bonus class with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises ScoValueError if class_name not found in module bonus_sport.
"""
if class_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
class_name = c.value
if class_name == "": # pas de bonus défini
return None
klass = bonus_spo.get_bonus_class_dict().get(class_name)
if klass is None:
raise ScoValueError(
f"""Fonction de calcul bonus sport inexistante: {class_name}.
(contacter votre administrateur local)."""
)
return klass
@classmethod
def get_bonus_sport_class_names(cls):
"""List available functions names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(bonus_spo.get_bonus_class_dict().keys())
@classmethod
def get_bonus_sport_func(cls):
"""Get configured bonus function, or None if None."""
return cls.get_bonus_sport_func_from_name()
@classmethod
def get_bonus_sport_func_from_name(cls, func_name=None):
"""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.
"""
if func_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
func_name = c.value
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:
@ -111,16 +144,3 @@ class ScoDocSiteConfig(db.Model):
f"""Fonction de calcul maison inexistante: {func_name}.
(contacter votre administrateur local)."""
)
@classmethod
def get_bonus_sport_func_names(cls):
"""List available functions names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(
[
getattr(bonus_sport, name).__name__
for name in dir(bonus_sport)
if name.startswith("bonus_")
]
)

View File

@ -41,6 +41,8 @@ class UniteEns(db.Model):
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float)
color = db.Column(db.Text())
# relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
modules = db.relationship("Module", lazy="dynamic", backref="ue")

View File

@ -73,7 +73,8 @@ def TrivialFormulator(
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest'
'boolcheckbox', 'text_suggest',
'color'
(default text)
size : text field width
rows, cols: textarea geometry
@ -594,6 +595,11 @@ class TF(object):
var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
"""
)
elif input_type == "color":
lem.append(
'<input type="color" name="%s" id="%s" %s' % (field, field, attribs)
)
lem.append(('value="%(' + field + ')s" >') % values)
else:
raise ValueError("unkown input_type for form (%s)!" % input_type)
explanation = descr.get("explanation", "")
@ -712,7 +718,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
R.append("%s</td>" % title)
R.append('<td class="tf-ro-field%s">' % klass)
if input_type == "text" or input_type == "text_suggest":
if (
input_type == "text"
or input_type == "text_suggest"
or input_type == "color"
):
R.append(("%(" + field + ")s") % self.values)
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
if input_type == "boolcheckbox":

View File

@ -77,7 +77,6 @@ def bonus_iutv(notes_sport, coefs, infos=None):
optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
# breakpoint()
bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10])
return bonus
@ -91,7 +90,7 @@ def bonus_direct(notes_sport, coefs, infos=None):
def bonus_iut_stdenis(notes_sport, coefs, infos=None):
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points."""
"""Semblable à bonus_iutv mais total limité à 0.5 points."""
points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10
bonus = points * 0.05 # ou / 20
return min(bonus, 0.5) # bonus limité à 1/2 point

View File

@ -29,6 +29,7 @@
"""
from html.parser import HTMLParser
from html.entities import name2codepoint
from multiprocessing.sharedctypes import Value
import re
from flask import g, url_for
@ -36,17 +37,23 @@ from flask import g, url_for
from . import listhistogram
def horizontal_bargraph(value, mark):
def horizontal_bargraph(value, mark) -> str:
"""html drawing an horizontal bar and a mark
used to vizualize the relative level of a student
"""
tmpl = """
try:
vals = {"value": int(value), "mark": int(mark)}
except ValueError:
return ""
return (
"""
<span class="graph">
<span class="bar" style="width: %(value)d%%;"></span>
<span class="mark" style="left: %(mark)d%%; "></span>
</span>
"""
return tmpl % {"value": int(value), "mark": int(mark)}
% vals
)
def histogram_notes(notes):

View File

@ -170,7 +170,7 @@ class NotesTable:
"""
def __init__(self, formsemestre_id):
log(f"NotesTable( formsemestre_id={formsemestre_id} )")
# log(f"NotesTable( formsemestre_id={formsemestre_id} )")
# raise NotImplementedError() # XXX
if not formsemestre_id:
raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
@ -909,6 +909,7 @@ class NotesTable:
if len(coefs_bonus_gen) == 1:
coefs_bonus_gen = [1.0] # irrelevant, may be zero
# XXX attention: utilise anciens bonus_sport, évidemment
bonus_func = ScoDocSiteConfig.get_bonus_sport_func()
if bonus_func:
bonus = bonus_func(

View File

@ -43,6 +43,7 @@ from flask import g, request
from flask import url_for
from flask_login import current_user
from flask_mail import Message
from app.models.moduleimpls import ModuleImplInscription
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
@ -285,19 +286,29 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
else:
I["rang_nt"], I["rang_txt"] = "", ""
I["note_max"] = 20.0 # notes toujours sur 20
I["bonus_sport_culture"] = nt.bonus[etudid]
I["bonus_sport_culture"] = nt.bonus[etudid] if nt.bonus is not None else 0.0
# Liste les UE / modules /evals
I["ues"] = []
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
):
continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
else:
x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True)
if nt.bonus is not None:
x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True)
else:
x = ""
if isinstance(x, str):
u["cur_moy_ue_txt"] = "pas de bonus"
else:

View File

@ -192,7 +192,9 @@ def formsemestre_bulletinetud_published_dict(
)
d["note_max"] = dict(value=20) # notes toujours sur 20
d["bonus_sport_culture"] = dict(value=nt.bonus[etudid])
d["bonus_sport_culture"] = dict(
value=nt.bonus[etudid] if nt.bonus is not None else 0.0
)
# Liste les UE / modules /evals
d["ue"] = []

View File

@ -195,7 +195,12 @@ def make_xml_formsemestre_bulletinetud(
)
)
doc.append(Element("note_max", value="20")) # notes toujours sur 20
doc.append(Element("bonus_sport_culture", value=str(nt.bonus[etudid])))
doc.append(
Element(
"bonus_sport_culture",
value=str(nt.bonus[etudid] if nt.bonus is not None else 0.0),
)
)
# Liste les UE / modules /evals
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
@ -211,7 +216,7 @@ def make_xml_formsemestre_bulletinetud(
if ue["type"] != sco_codes_parcours.UE_SPORT:
v = ue_status["cur_moy_ue"]
else:
v = nt.bonus[etudid]
v = nt.bonus[etudid] if nt.bonus is not None else 0.0
x_ue.append(
Element(
"note",

View File

@ -98,8 +98,9 @@ class ScoDocCache:
status = CACHE.set(key, value, timeout=cls.timeout)
if not status:
log("Error: cache set failed !")
except:
except Exception as exc:
log("XXX CACHE Warning: error in set !!!")
log(exc)
status = None
return status

View File

@ -168,7 +168,7 @@ class BonusSportUpdate(Action):
def build_action(parameters):
if (
parameters["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
!= ScoDocSiteConfig.get_bonus_sport_class_name()
):
return [BonusSportUpdate(parameters)]
return []

View File

@ -81,6 +81,7 @@ _ueEditor = ndb.EditableTable(
"is_external",
"code_apogee",
"coefficient",
"color",
),
sortkey="numero",
input_formators={
@ -358,6 +359,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
},
),
(
"color",
{
"input_type": "color",
"title": "Couleur",
"explanation": "pour affichages",
},
),
]
if create and not parcours.UE_IS_MODULE and not is_apc:
fw.append(

View File

@ -1107,6 +1107,7 @@ _TABLEAU_MODULES_HEAD = """
<th class="formsemestre_status">Module</th>
<th class="formsemestre_status">Inscrits</th>
<th class="resp">Responsable</th>
<th class="coef">Coefs.</th>
<th class="evals">Évaluations</th>
</tr>
"""
@ -1213,7 +1214,21 @@ def formsemestre_tableau_modules(
sco_users.user_info(modimpl["responsable_id"])["prenomnom"],
)
)
H.append("<td>")
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
coefs = mod.ue_coefs_list()
for coef in coefs:
if coef[1] > 0:
H.append(
f"""<span class="mod_coef_indicator"
title="{coef[0].acronyme}"
style="background: {
coef[0].color if coef[0].color is not None else 'blue'
}"></span>"""
)
else:
H.append(f"""<span class="mod_coef_indicator_zero"></span>""")
H.append("</td>")
if mod.module_type in (
None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs
ModuleType.STANDARD,

View File

@ -37,7 +37,10 @@ from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.comp import res_sem
from app.comp import moy_mod
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_common import NotesTableCompat
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
@ -432,7 +435,7 @@ def _make_table_notes(
if is_apc:
# Ajoute une colonne par UE
_add_apc_columns(
moduleimpl_id,
modimpl,
evals_poids,
ues,
rows,
@ -815,7 +818,7 @@ def _add_moymod_column(
def _add_apc_columns(
moduleimpl_id,
modimpl,
evals_poids,
ues,
rows,
@ -834,18 +837,23 @@ def _add_apc_columns(
# => On recharge tout dans les nouveaux modèles
# rows est une liste de dict avec une clé "etudid"
# on va y ajouter une clé par UE du semestre
modimpl = ModuleImpl.query.get(moduleimpl_id)
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
moduleimpl_id
)
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
nt: NotesTableCompat = res_sem.load_formsemestre_result(modimpl.formsemestre)
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
# XXX A ENLEVER TODO
# modimpl = ModuleImpl.query.get(moduleimpl_id)
# evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
# moduleimpl_id
# )
# etuds_moy_module = moy_mod.compute_module_moy(
# evals_notes, evals_poids, evaluations, evaluations_completes
# )
if is_conforme:
# valeur des moyennes vers les UEs:
for row in rows:
for ue in ues:
moy_ue = etuds_moy_module[ue.id].get(row["etudid"], "?")
moy_ue = modimpl_results.etuds_moy_module[ue.id].get(row["etudid"], "?")
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
# Nom et coefs des UE (lignes titres):

View File

@ -171,7 +171,9 @@ def _ue_coefs_html(coefs_lst) -> str:
"""
+ "\n".join(
[
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue.acronyme}</div>"""
f"""<div style="--coef:{coef};
{'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
]
)

View File

@ -1308,6 +1308,20 @@ td.formsemestre_status_cell {
white-space: nowrap;
}
span.mod_coef_indicator, span.ue_color_indicator {
display:inline-block;
width: 10px;
height: 10px;
}
span.mod_coef_indicator_zero {
display:inline-block;
width: 9px;
height: 9px;
border: 1px solid rgb(156, 156, 156);
}
span.status_ue_acro { font-weight: bold; }
span.status_ue_title { font-style: italic; padding-left: 1cm;}
span.status_module_cat { font-weight: bold; }

View File

@ -30,6 +30,8 @@
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a>
<span class="ue_type_{{ue.type}}">
<span class="ue_color_indicator" style="background:{{
ue.color if ue.color is not none else 'blue'}}"></span>
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
>{{ue.titre}}</a>

View File

@ -0,0 +1,28 @@
"""couleur UE
Revision ID: c95d5a3bf0de
Revises: f40fbaf5831c
Create Date: 2022-01-24 21:44:55.205544
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "c95d5a3bf0de"
down_revision = "f40fbaf5831c"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("notes_ue", sa.Column("color", sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("notes_ue", "color")
# ### end Alembic commands ###

View File

@ -133,7 +133,7 @@ def user_create(username, role, dept, nom=None, prenom=None): # user-create
"Create a new user"
r = Role.get_named_role(role)
if not r:
sys.stderr.write("user_create: role {r} does not exists\n".format(r=role))
sys.stderr.write("user_create: role {r} does not exist\n".format(r=role))
return 1
u = User.query.filter_by(user_name=username).first()
if u: