Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into dev93

This commit is contained in:
Emmanuel Viennet 2022-06-09 07:40:10 +02:00
commit 9cc7a80fb0
6 changed files with 122 additions and 104 deletions

View File

@ -730,7 +730,7 @@ class BonusIUTRennes1(BonusSportAdditif):
seuil_moy_gen = 10.0
proportion_point = 1 / 20.0
classic_use_bonus_ues = False
# Adapté de BonusTarbes, mais s'applique aussi en classic
# S'applique aussi en classic, sur la moy. gen.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Prend la note de chaque modimpl, sans considération d'UE
@ -771,18 +771,59 @@ class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
<ul>
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
<li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li>
<li>Si la note de sport est comprise entre 0 et 10 : pas dajout de point.</li>
<li>Si la note de sport est comprise entre 10 et 20 :
<ul>
<li>Pour le BUT, application pour chaque UE du semestre :
<ul>
<li>pour une note entre 18 et 20 => + 0,10 points</li>
<li>pour une note entre 16 et 17,99 => + 0,08 points</li>
<li>pour une note entre 14 et 15,99 => + 0,06 points</li>
<li>pour une note entre 12 et 13,99 => + 0,04 points</li>
<li>pour une note entre 10 et 11,99 => + 0,02 points</li>
</ul>
</li>
<li>Pour les DUT/LP :
ajout de 1% de la note sur la moyenne générale du semestre
</li>
</ul>
</li>
</ul>
"""
name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # si bonus > 10,
seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1%
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# La date du semestre ?
if self.formsemestre.formation.is_apc():
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.0] = 0.10
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.08
bonus_moy_arr[bonus_moy_arr >= 14.0] = 0.06
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.04
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.02
self.bonus_additif(bonus_moy_arr)
else:
# DUT et LP:
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusLeHavre(BonusSportAdditif):
"""Bonus sport IUT du Havre sur les moyennes d'UE
@ -1067,14 +1108,15 @@ class BonusStNazaire(BonusSportMultiplicatif):
factor_max = 0.1 # 10% max
class BonusTarbes(BonusSportAdditif):
class BonusTarbes(BonusIUTRennes1):
"""Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
<ul>
<li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li>
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE.
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE en BUT,
ou à la moyenne générale en DUT et LP.
</li>
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
sur chaque UE.
@ -1088,29 +1130,6 @@ class BonusTarbes(BonusSportAdditif):
proportion_point = 1 / 30.0
classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours.

View File

@ -41,7 +41,8 @@ from app import db
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoBugCatcher
from app.scodoc.sco_utils import ModuleType
@ -423,7 +424,9 @@ def moduleimpl_is_conforme(
if nb_ues == 0:
return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues:
raise ValueError("moduleimpl_is_conforme: nb ue incoherent")
# il arrive (#bug) que le cache ne soit pas à jour...
sco_cache.invalidate_formsemestre()
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all(
(modules_coefficients[moduleimpl.module_id].to_numpy() != 0)

View File

@ -79,6 +79,15 @@ class UniteEns(db.Model):
return sco_edit_ue.ue_is_locked(self.id)
def can_be_deleted(self) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (self.modules.count() == 0) or not any(
m.modimpls.all() for m in self.modules
)
def guess_semestre_idx(self) -> None:
"""Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre.

View File

@ -66,8 +66,9 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
if sems:
H.append(
"""<p class="warning">Impossible de supprimer cette formation, car les sessions suivantes l'utilisent:</p>
<ul>"""
"""<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p>
<ul>"""
)
for sem in sems:
H.append(

View File

@ -204,7 +204,7 @@ def module_delete(module_id=None):
H = [
html_sco_header.sco_header(page_title="Suppression d'un module"),
f"""<h2>Suppression du module {module.titre} ({module.code})</h2>""",
f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"} ({module.code})</h2>""",
]
dest_url = url_for(
@ -917,21 +917,13 @@ def module_count_moduleimpls(module_id):
def formation_add_malus_modules(formation_id, titre=None, redirect=True):
"""Création d'un module de "malus" dans chaque UE d'une formation"""
from app.scodoc import sco_edit_ue
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id})
formation = Formation.query.get_or_404(formation_id)
for ue in ues:
# Un seul module de malus par UE:
nb_mod_malus = len(
[
mod
for mod in module_list(args={"ue_id": ue["ue_id"]})
if mod["module_type"] == scu.ModuleType.MALUS
]
)
if nb_mod_malus == 0:
ue_add_malus_module(ue["ue_id"], titre=titre)
for ue in formation.ues:
ue_add_malus_module(ue, titre=titre)
formation.invalidate_cached_sems()
if redirect:
return flask.redirect(
@ -941,46 +933,58 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
)
def ue_add_malus_module(ue_id, titre=None, code=None):
"""Add a malus module in this ue"""
from app.scodoc import sco_edit_ue
def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
"""Add a malus module in this ue.
If already exists, do nothing.
Returns id of malus module.
"""
modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS]
if len(modules_malus) > 0:
return modules_malus[0].id # déjà existant
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
if titre is None:
titre = ""
if code is None:
code = "MALUS%d" % ue["numero"]
titre = titre or ""
code = code or f"MALUS{ue.numero}"
# Tout module doit avoir un semestre_id (indice 1, 2, ...)
semestre_ids = sco_edit_ue.ue_list_semestre_ids(ue)
if semestre_ids:
semestre_id = semestre_ids[0]
if ue.semestre_idx is None:
semestre_ids = sorted(list(set([m.semestre_id for m in ue.modules])))
if len(semestre_ids) > 0:
semestre_id = semestre_ids[0]
else:
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
# le semestre ? ou affecter le malus au semestre 1 ???
raise ScoValueError(
"Impossible d'ajouter un malus s'il n'y a pas d'autres modules"
)
else:
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
# le semestre ? ou affecter le malus au semestre 1 ???
raise ScoValueError(
"Impossible d'ajouter un malus s'il n'y a pas d'autres modules"
)
semestre_id = ue.semestre_idx
# Matiere pour placer le module malus
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id})
numero = max([mat["numero"] for mat in Matlist]) + 10
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": "Malus", "numero": numero}
)
titre_matiere_malus = "Malus"
module_id = do_module_create(
{
"titre": titre,
"code": code,
"coefficient": 0.0, # unused
"ue_id": ue_id,
"matiere_id": matiere_id,
"formation_id": ue["formation_id"],
"semestre_id": semestre_id,
"module_type": scu.ModuleType.MALUS,
},
)
matieres_malus = [mat for mat in ue.matieres if mat.titre == titre_matiere_malus]
if len(matieres_malus) > 0:
# matière Malus déjà existante, l'utilise
matiere = matieres_malus[0]
else:
if ue.matieres.count() > 0:
numero = max([mat.numero for mat in ue.matieres]) + 10
else:
numero = 0
matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero)
db.session.add(matiere)
return module_id
module = Module(
titre=titre,
code=code,
coefficient=0.0,
ue=ue,
matiere=matiere,
formation=ue.formation,
semestre_id=semestre_id,
module_type=scu.ModuleType.MALUS,
)
db.session.add(module)
db.session.commit()
return module.id

View File

@ -143,14 +143,6 @@ def do_ue_create(args):
return ue_id
def can_delete_ue(ue: UniteEns) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (ue.modules.count() == 0) and not any(m.modimpls.all() for m in ue.modules)
def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations
@ -159,9 +151,9 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
ue = UniteEns.query.get_or_404(ue_id)
formation_id = ue.formation_id
semestre_idx = ue.semestre_idx
if not can_delete_ue(ue):
if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject(
"UE",
f"UE (id={ue.id}, dud)",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
@ -553,9 +545,9 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
semestre_idx=ue.semestre_idx,
),
)
if not can_delete_ue(ue):
if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject(
"UE",
f"UE",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
@ -1367,16 +1359,6 @@ def ue_is_locked(ue_id):
return len(r) > 0
def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
aussi ScoDoc laisse le choix.
"""
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in modules])))
UE_PALETTE = [
"#B80004", # rouge
"#F97B3D", # Orange Crayola