Merge branch 'refactor_nt' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-02-01 14:14:39 +01:00
commit e85149f4a6
19 changed files with 213 additions and 49 deletions

View File

@ -71,7 +71,7 @@ class BulletinBUT(ResultatsSemestreBUT):
"bonus": fmt_note(self.bonus_ues[ue.id][etud.id])
if self.bonus_ues is not None and ue.id in self.bonus_ues
else fmt_note(0.0),
"malus": None, # XXX TODO voir ce qui est ici
"malus": self.malus[ue.id][etud.id],
"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),

View File

@ -104,7 +104,6 @@ class BonusSport:
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
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)
@ -124,7 +123,7 @@ class BonusSport:
# 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),
np.stack([modimpl_coefs_spo] * nb_etuds),
0.0,
)
else:
@ -162,7 +161,7 @@ class BonusSport:
"""
raise NotImplementedError("méthode virtuelle")
def get_bonus_ues(self) -> pd.Series:
def get_bonus_ues(self) -> pd.DataFrame:
"""Les bonus à appliquer aux UE
Résultat: DataFrame de float, index etudid, columns: ue.id
"""

View File

@ -43,6 +43,8 @@ from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
@dataclass
class EvaluationEtat:
@ -233,6 +235,8 @@ class ModuleImplResultsAPC(ModuleImplResults):
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
if nb_ues == 0:
return pd.DataFrame(index=self.evals_notes.index, columns=[])
evals_coefs = self.get_evaluations_coefs(moduleimpl)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
@ -289,7 +293,12 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés:
default_poids = 1.0 if modimpl.module.ue.type == UE_SPORT else 0.0
default_poids = (
1.0
if modimpl.module.ue.type == UE_SPORT
or modimpl.module.module_type == ModuleType.MALUS
else 0.0
)
if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict()

View File

@ -27,6 +27,7 @@
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
"""
from re import X
import numpy as np
import pandas as pd
@ -263,6 +264,7 @@ def compute_ue_moys_apc(
#
# Version vectorisée
#
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = np.sum(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
@ -379,3 +381,42 @@ def compute_ue_moys_classic(
etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index)
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
ues: list[UniteEns],
modimpl_inscr_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcul le malus sur les UE
Dans chaque UE, on peut avoir un ou plusieurs modules de MALUS.
Leurs notes sont positives ou négatives. leur somme sera _soustraite_ à la moyenne
de chaque UE.
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 non bonus)
- ues: les ues du semestre (incluant le bonus sport)
- modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
Résultat: DataFrame de float, index etudid, columns: ue.id (sans NaN)
"""
ues_idx = [ue.id for ue in ues]
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
for ue in ues:
if ue.type != UE_SPORT:
modimpl_mask = np.array(
[
(m.module.module_type == ModuleType.MALUS)
and (m.module.ue.id == ue.id)
for m in formsemestre.modimpls_sorted
]
)
malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1)
malus[ue.id] = malus_moys
malus.fillna(0.0, inplace=True)
return malus

View File

@ -68,6 +68,12 @@ class ResultatsSemestreBUT(NotesTableCompat):
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
# --- Modules de MALUS sur les UEs
self.malus = moy_ue.compute_malus(
self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df
)
self.etud_moy_ue -= self.malus
# --- Bonus Sport & Culture
if len(modimpls_sport) > 0:
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()

View File

@ -71,6 +71,16 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.modimpl_coefs,
modimpl_standards_mask,
)
# --- Modules de MALUS sur les UEs et la moyenne générale
self.malus = moy_ue.compute_malus(
self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df
)
self.etud_moy_ue -= self.malus
# ajuste la moyenne générale (à l'aide des coefs d'UE)
self.etud_moy_gen -= (self.etud_coef_ue_df * self.malus).sum(
axis=1
) / self.etud_coef_ue_df.sum(axis=1)
# --- Bonus Sport & Culture
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:

View File

@ -70,7 +70,7 @@ class CodesDecisionsForm(FlaskForm):
ATT = _build_code_field("ATT")
CMP = _build_code_field("CMP")
DEF = _build_code_field("DEF")
DEM = _build_code_field("DEF")
DEM = _build_code_field("DEM")
NAR = _build_code_field("NAR")
RAT = _build_code_field("RAT")
submit = SubmitField("Valider")

View File

@ -178,7 +178,7 @@ class ScoDocSiteConfig(db.Model):
return getattr(bonus_sport, func_name)
except AttributeError:
raise ScoValueError(
f"""Fonction de calcul maison inexistante: {func_name}.
f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}".
(contacter votre administrateur local)."""
)

View File

@ -276,14 +276,24 @@ class TF(object):
)
ok = 0
if typ[:3] == "int" or typ == "float" or typ == "real":
if "min_value" in descr and val < descr["min_value"]:
if (
val != ""
and val != None
and "min_value" in descr
and val < descr["min_value"]
):
msg.append(
"La valeur (%d) du champ '%s' est trop petite (min=%s)"
% (val, field, descr["min_value"])
)
ok = 0
if "max_value" in descr and val > descr["max_value"]:
if (
val != ""
and val != None
and "max_value" in descr
and val > descr["max_value"]
):
msg.append(
"La valeur (%s) du champ '%s' est trop grande (max=%s)"
% (val, field, descr["max_value"])

View File

@ -456,6 +456,11 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None):
return bonus
def bonus_iutlemans(notes_sport, coefs, infos=None):
"fake: formule inutilisée en ScoDoc 9.2 mais doiut être présente"
return 0.0
def bonus_iutlr(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point

View File

@ -125,8 +125,8 @@ APO_NEWLINE = "\r\n"
def _apo_fmt_note(note):
"Formatte une note pour Apogée (séparateur décimal: ',')"
if not note and isinstance(note, float):
return ""
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
# return ""
try:
val = float(note)
except ValueError:

View File

@ -141,7 +141,7 @@ BUG = "BUG"
ALL = "ALL"
# Explication des codes (de demestre ou d'UE)
# Explication des codes (de semestre ou d'UE)
CODES_EXPL = {
ADC: "Validé par compensation",
ADJ: "Validé par le Jury",
@ -154,6 +154,7 @@ CODES_EXPL = {
DEF: "Défaillant",
NAR: "Échec, non autorisé à redoubler",
RAT: "En attente d'un rattrapage",
DEM: "Démission",
}
# Nota: ces explications sont personnalisables via le fichier
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py

View File

@ -115,22 +115,30 @@ def do_module_create(args) -> int:
return r
def module_create(matiere_id=None, module_type=None, semestre_id=None):
"""Création d'un module"""
def module_create(
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
):
"""Formulaire de création d'un module
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée).
"""
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
if matiere is None:
raise ScoValueError("invalid matiere !")
ue = matiere.ue
parcours = ue.formation.get_parcours()
formation = ue.formation
else:
formation = Formation.query.get_or_404(formation_id)
parcours = formation.get_parcours()
is_apc = parcours.APC_SAE
ues = ue.formation.ues.order_by(
ues = formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# cherche le numero adéquat (pour placer le module en fin de liste)
modules = matiere.ue.formation.modules.all()
modules = formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
@ -143,9 +151,11 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
H = [
html_sco_header.sco_header(page_title=f"Création {object_name}"),
]
if is_apc:
if not matiere_id:
H += [
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}, Semestre {ue.semestre_idx}, {ue.acronyme}</h2>"""
f"""<h2>Création {object_name} dans la formation {formation.acronyme}
</h2>
"""
]
else:
H += [
@ -158,7 +168,6 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
ue=ue,
semestre_id=semestre_id,
)
]
@ -170,7 +179,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"size": 10,
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
"allow_null": False,
"validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id
),
},
@ -192,6 +201,15 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
]
semestres_indices = list(range(1, parcours.NB_SEM + 1))
if is_apc:
module_types = scu.ModuleType # tous les types
else:
# ne propose pas SAE et Ressources:
module_types = set(scu.ModuleType) - {
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
}
descr += [
(
"module_type",
@ -199,8 +217,8 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
},
),
(
@ -256,11 +274,30 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
),
]
if matiere_id:
descr += [
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
]
else:
# choix de l'UE de rattachement
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
"allowed_values": [u.id for u in ues],
},
),
]
descr += [
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
("formation_id", {"default": formation.id, "input_type": "hidden"}),
(
"code_apogee",
{
@ -290,6 +327,20 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
else:
if not matiere_id:
# formulaire avec choix UE de rattachement
ue = UniteEns.query.get(tf[2]["ue_id"])
if ue is None:
raise ValueError("UE invalide")
matiere = ue.matieres.first()
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
)
tf[2]["matiere_id"] = matiere_id
tf[2]["semestre_id"] = ue.semestre_idx
_ = do_module_create(tf[2])
@ -298,7 +349,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
formation_id=formation.id,
semestre_idx=tf[2]["semestre_id"],
)
)
@ -493,6 +544,13 @@ def module_edit(module_id=None):
soyez prudents !
</span></div>"""
)
if is_apc:
module_types = scu.ModuleType # tous les types
else:
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
) | {a_module.module_type}
descr = [
(
@ -514,8 +572,8 @@ def module_edit(module_id=None):
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
"enabled": unlocked,
},
),

View File

@ -998,6 +998,7 @@ def _ue_table_matieres(
H.append(
_ue_table_modules(
parcours,
ue,
mat,
modules,
editable,
@ -1031,6 +1032,7 @@ def _ue_table_matieres(
def _ue_table_modules(
parcours,
ue,
mat,
modules,
editable,
@ -1121,8 +1123,12 @@ def _ue_table_modules(
tag_cls,
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
)
if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
warning_semestre = ' <span class="red">incohérent ?</span>'
else:
warning_semestre = ""
H.append(
" %s %s" % (parcours.SESSION_NAME, mod["semestre_id"])
" %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
+ " (%s)" % heurescoef
+ tag_edit
)

View File

@ -546,7 +546,7 @@ def do_formsemestre_createwithmodules(edit=False):
for mod in mods:
if mod["semestre_id"] == semestre_id and (
(not edit) # creation => tous modules
or (not formation.is_apc()) # pas BUT, on peux mixer les semestres
or (not formation.is_apc()) # pas BUT, on peut mixer les semestres
or (semestre_id == formsemestre.semestre_id) # module du semestre
or (mod["module_id"] in module_ids_set) # module déjà présent
):

View File

@ -219,7 +219,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}"
),
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{Mod['code']}</tt> {Mod['titre']}</h2>
<tt>{Mod['code']}</tt> {Mod['titre']}
{"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""}
</h2>
<div class="moduleimpl_tableaubord moduleimpl_type_{
scu.ModuleType(Mod['module_type']).name.lower()}">
<table>

View File

@ -71,13 +71,21 @@
</li>
{% endfor %}
{% if editable and matiere_parent %}
<li><a class="stdlink" href="{{
{% if editable %}
<li><a class="stdlink" href=
{% if matiere_parent %}"{{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept,
module_type=module_type|int,
matiere_id=matiere_parent.id
)}}"
{% else %}"{{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept,
module_type=module_type|int,
formation_id=formation.id
)}}"
{% endif %}
>{{create_element_msg}}</a>
</li>
{% endif %}

View File

@ -191,7 +191,7 @@ def get_etud_dept():
# le choix a peu d'importance...
last_etud = etuds[-1]
return Departement.query.get(last_etud.dept_id).acronym
return Departement.query.get_or_404(last_etud.dept_id).acronym
# Bricolage pour le portail IUTV avec ScoDoc 7: (DEPRECATED: NE PAS UTILISER !)

View File

@ -149,7 +149,7 @@ def user_info(user_name, format="json"):
@scodoc
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func
def create_user_form(user_name=None, edit=0, all_roles=1):
def create_user_form(user_name=None, edit=0, all_roles=False):
"form. création ou édition utilisateur"
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
@ -218,9 +218,11 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
}
if current_user.is_administrator():
editable_roles_set |= {
(Role.get_named_role(r), "")
(Role.get_named_role(r), None)
for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC
}
# Un super-admin peut nommer d'autres super-admin:
editable_roles_set |= {(Role.get_named_role("SuperAdmin"), None)}
#
if not edit:
submitlabel = "Créer utilisateur"
@ -251,16 +253,23 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
orig_roles_strings = {r.name + "_" + (dept or "") for (r, dept) in orig_roles}
# add existing user roles
displayed_roles = list(editable_roles_set.union(orig_roles))
displayed_roles.sort(key=lambda x: (x[1] or "", x[0].name or ""))
displayed_roles.sort(
key=lambda x: (
x[1] or "",
(x[0].name or "") if x[0].name != "SuperAdmin" else "A",
)
)
displayed_roles_strings = [
r.name + "_" + (dept or "") for (r, dept) in displayed_roles
]
displayed_roles_labels = [f"{dept}: {r.name}" for (r, dept) in displayed_roles]
displayed_roles_labels = [
f"{dept or '<em>tout dépt.</em>'}: {r.name}" for (r, dept) in displayed_roles
]
disabled_roles = {} # pour désactiver les roles que l'on ne peut pas éditer
for i in range(len(displayed_roles_strings)):
if displayed_roles_strings[i] not in editable_roles_strings:
disabled_roles[i] = True
breakpoint()
descr = [
("edit", {"input_type": "hidden", "default": edit}),
("nom", {"title": "Nom", "size": 20, "allow_null": False}),