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

This commit is contained in:
Arthur ZHU 2022-02-02 19:14:17 +01:00
commit 2cc5e1f164
12 changed files with 198 additions and 117 deletions

View File

@ -41,7 +41,6 @@ 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.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
@ -92,6 +91,10 @@ class ModuleImplResults:
ne donnent pas de coef vers cette UE.
"""
self.load_notes()
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
def load_notes(self): # ré-écriture de df_load_modimpl_notes
"""Charge toutes les notes de toutes les évaluations du module.
@ -135,8 +138,11 @@ class ModuleImplResults:
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
# ou évaluation déclarée "à prise en compte immédiate"
is_complete = evaluation.publish_incomplete or (
not (inscrits_module - set(eval_df.index))
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes
# car on calcule leur moyenne à part.
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
evaluation.publish_incomplete
or (not (inscrits_module - set(eval_df.index)))
)
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
@ -212,6 +218,33 @@ class ModuleImplResults:
self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl):
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la note eval rattrapage.
"""
eval_list = [
e
for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE
]
if eval_list:
return eval_list[0]
return None
def get_evaluation_session2(self, moduleimpl: ModuleImpl):
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
Session 2: remplace la note de moyenne des autres évals.
"""
eval_list = [
e
for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_SESSION2
]
if eval_list:
return eval_list[0]
return None
class ModuleImplResultsAPC(ModuleImplResults):
"Calcul des moyennes de modules à la mode BUT"
@ -229,7 +262,7 @@ class ModuleImplResultsAPC(ModuleImplResults):
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE.
"""
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
nb_etuds, nb_evals = self.evals_notes.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
@ -237,11 +270,11 @@ class ModuleImplResultsAPC(ModuleImplResults):
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_coefs = self.get_evaluations_coefs(modimpl)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
evals_notes_20 = self.get_eval_notes_sur_20(moduleimpl)
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
# Les poids des évals pour chaque étudiant: là où il a des notes
# non neutralisées
@ -262,6 +295,45 @@ class ModuleImplResultsAPC(ModuleImplResults):
etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, axis=1)
# Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2:
notes_session2 = self.evals_notes[eval_session2.id].values
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
etuds_moy_module = np.where(
etuds_use_session2[:, np.newaxis],
np.tile(
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
nb_ues,
),
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
else:
# Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl)
if eval_rat:
notes_rat = self.evals_notes[eval_rat.id].values
# remplace les notes invalides (ATT, EXC...) par des NaN
notes_rat = np.where(
notes_rat > scu.NOTES_ABSENCE,
notes_rat / (eval_rat.note_max / 20.0),
np.nan,
)
# prend le max
etuds_use_rattrapage = notes_rat > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage[:, np.newaxis],
np.tile(notes_rat[:, np.newaxis], nb_ues),
etuds_moy_module,
)
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
)
self.etuds_moy_module = pd.DataFrame(
etuds_moy_module,
index=self.evals_notes.index,
@ -371,8 +443,42 @@ class ModuleImplResultsClassic(ModuleImplResults):
evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, axis=1)
# Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2:
notes_session2 = self.evals_notes[eval_session2.id].values
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
etuds_moy_module = np.where(
etuds_use_session2,
notes_session2 / (eval_session2.note_max / 20.0),
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
else:
# Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl)
if eval_rat:
notes_rat = self.evals_notes[eval_rat.id].values
# remplace les notes invalides (ATT, EXC...) par des NaN
notes_rat = np.where(
notes_rat > scu.NOTES_ABSENCE,
notes_rat / (eval_rat.note_max / 20.0),
np.nan,
)
# prend le max
etuds_use_rattrapage = notes_rat > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage, notes_rat, etuds_moy_module
)
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
)
self.etuds_moy_module = pd.Series(
etuds_moy_module,
index=self.evals_notes.index,
)
return self.etuds_moy_module

View File

@ -162,6 +162,7 @@ class NotesTableCompat(ResultatsSemestre):
_cached_attrs = ResultatsSemestre._cached_attrs + (
"bonus",
"bonus_ues",
"malus",
)
def __init__(self, formsemestre: FormSemestre):

View File

@ -22,6 +22,7 @@ from app.models.etudiants import Identite
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_permissions import Permission
class FormSemestre(db.Model):
@ -169,14 +170,24 @@ class FormSemestre(db.Model):
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
m.module.ue.numero or 0,
m.module.matiere.numero or 0,
m.module.numero or 0,
m.module.code or "",
)
)
return modimpls
def can_be_edited_by(self, user):
"""Vrai si user peut modifier ce semestre"""
if not user.has_permission(Permission.ScoImplement): # pas chef
if not self.resp_can_edit or user.id not in [
resp.id for resp in self.responsables
]:
return False
return True
def est_courant(self) -> bool:
"""Vrai si la date actuelle (now) est dans le semestre
(les dates de début et fin sont incluses)
@ -425,7 +436,7 @@ class FormSemestreUECoef(db.Model):
class FormSemestreUEComputationExpr(db.Model):
"""Formules utilisateurs pour calcul moyenne UE"""
"""Formules utilisateurs pour calcul moyenne UE (désactivées en 9.2+)."""
__tablename__ = "notes_formsemestre_ue_computation_expr"
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),)

View File

@ -619,7 +619,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
prefs=prefs,
)
if nbeval: # boite autour des evaluations (en pdf)
if nbeval: # boite autour des évaluations (en pdf)
P[-1]["_pdf_style"].append(
("BOX", (1, 1 - nbeval), (-1, 0), 0.2, self.PDF_LIGHT_GRAY)
)

View File

@ -289,7 +289,10 @@ def module_create(
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
"labels": [
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}"
for u in ues
],
"allowed_values": [u.id for u in ues],
},
),

View File

@ -231,13 +231,17 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
return None
def ue_create(formation_id=None):
"""Creation d'une UE"""
return ue_edit(create=True, formation_id=formation_id)
def ue_create(formation_id=None, default_semestre_idx=None):
"""Formulaire création d'une UE"""
return ue_edit(
create=True,
formation_id=formation_id,
default_semestre_idx=default_semestre_idx,
)
def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou création d'une UE"""
def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=None):
"""Formulaire modification ou création d'une UE"""
create = int(create)
if not create:
U = ue_list(args={"ue_id": ue_id})
@ -250,7 +254,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
submitlabel = "Modifier les valeurs"
else:
title = "Création d'une UE"
initvalues = {}
initvalues = {"semestre_idx": default_semestre_idx}
submitlabel = "Créer cette UE"
formation = Formation.query.get(formation_id)
if not formation:

View File

@ -207,7 +207,7 @@ def evaluation_create_form(
{
"size": 6,
"type": "float",
"explanation": "coef. dans le module (choisi librement par l'enseignant)",
"explanation": "coef. dans le module (choisi librement par l'enseignant, non utilisé pour rattrapage et 2ème session)",
"allow_null": False,
},
)

View File

@ -118,10 +118,16 @@ def formsemestre_editwithmodules(formsemestre_id):
vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
H.append(
"""<p class="help">Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier".
</p>
<p class="help">Attention : s'il y a déjà des évaluations dans un module, il ne peut pas être supprimé !</p>
<p class="help">Les modules ont toujours un responsable. Par défaut, c'est le directeur des études.</p>"""
"""<p class="help">Seuls les modules cochés font partie de ce semestre.
Pour les retirer, les décocher et appuyer sur le bouton "modifier".
</p>
<p class="help">Attention : s'il y a déjà des évaluations dans un module,
il ne peut pas être supprimé !</p>
<p class="help">Les modules ont toujours un responsable.
Par défaut, c'est le directeur des études.</p>
<p class="help">Un semestre ne peut comporter qu'une seule UE "bonus
sport/culture"</p>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
@ -739,6 +745,7 @@ def do_formsemestre_createwithmodules(edit=False):
# Modules sélectionnés:
# (retire le "MI" du début du nom de champs)
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
_formsemestre_check_ue_bonus_unicity(module_ids_checked)
if not edit:
if formation.is_apc():
_formsemestre_check_module_list(
@ -882,6 +889,18 @@ def _formsemestre_check_module_list(module_ids, semestre_idx):
)
def _formsemestre_check_ue_bonus_unicity(module_ids):
"""Vérifie qu'il n'y a qu'une seule UE bonus associée aux modules choisis"""
ues = [Module.query.get_or_404(module_id).ue for module_id in module_ids]
ues_bonus = {ue.id for ue in ues if ue.type == sco_codes_parcours.UE_SPORT}
if len(ues_bonus) > 1:
raise ScoValueError(
"""Les modules de bonus sélectionnés ne sont pas tous dans la même UE bonus.
Changez la sélection ou modifiez la structure du programme de formation.""",
dest_url="javascript:history.back();",
)
def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
"""Delete moduleimpls
module_ids_to_del: list of module_id (warning: not moduleimpl)

View File

@ -1152,30 +1152,19 @@ def formsemestre_tableau_modules(
f"""<tr class="formsemestre_status_ue"><td colspan="4">
<span class="status_ue_acro">{ue["acronyme"]}</span>
<span class="status_ue_title">{titre}</span>
</td><td>"""
</td><td colspan="2">"""
)
if can_edit:
H.append(
' <a href="edit_ue_expr?formsemestre_id=%s&ue_id=%s">'
% (formsemestre_id, ue["ue_id"])
)
H.append(
scu.icontag(
"formula",
title="Mode calcul moyenne d'UE",
style="vertical-align:middle",
)
)
if can_edit:
H.append("</a>")
expr = sco_compute_moy.get_ue_expression(
formsemestre_id, ue["ue_id"], html_quote=True
)
if expr:
H.append(
""" <span class="formula" title="mode de calcul de la moyenne d'UE">%s</span>"""
% expr
f""" <span class="formula" title="mode de calcul de la moyenne d'UE">{expr}</span>
<span class="warning">formule inutilisée en 9.2: <a href="{
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ue_id=ue["ue_id"] )
}
">supprimer</a></span>"""
)
H.append("</td></tr>")

View File

@ -46,16 +46,17 @@
</li>
{% endfor %}
</ul>
{% endfor %}
{% if editable %}
<ul>
<li class="notes_ue_list notes_ue_list_add"><a class="stdlink" href="{{
url_for('notes.ue_create',
scodoc_dept=g.scodoc_dept,
formation_id=formation.id,
default_semestre_idx=semestre_idx,
)}}"
>ajouter une UE</a>
</li>
</ul>
{% endif %}
{% endfor %}
</div>

View File

@ -30,23 +30,19 @@ Module notes: issu de ScoDoc7 / ZNotes.py
Emmanuel Viennet, 2021
"""
import sys
import time
import datetime
import pprint
from operator import itemgetter
from xml.etree import ElementTree
import flask
from flask import url_for, jsonify, render_template
from flask import flash, jsonify, render_template, url_for
from flask import current_app, g, request
from flask_login import current_user
from werkzeug.utils import redirect
from app.models.formsemestre import FormSemestre
from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.ues import UniteEns
from config import Config
from app import api
from app import db
from app import models
@ -1245,76 +1241,28 @@ def view_module_abs(moduleimpl_id, format="html"):
return "\n".join(H) + tab.html() + html_sco_header.sco_footer()
@bp.route("/edit_ue_expr", methods=["GET", "POST"])
@bp.route("/delete_ue_expr/<int:formsemestre_id>/<int:ue_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def edit_ue_expr(formsemestre_id, ue_id):
"""Edition formule calcul moyenne UE"""
# Check access
sem = sco_formsemestre_edit.can_edit_sem(formsemestre_id)
if not sem:
def delete_ue_expr(formsemestre_id: int, ue_id: int):
"""Efface une expression de calcul d'UE"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_be_edited_by(current_user):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
cnx = ndb.GetDBConnexion()
#
ue = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
H = [
html_sco_header.html_sem_header(
"Modification règle de calcul de l'UE %s (%s)"
% (ue["acronyme"], ue["titre"]),
),
_EXPR_HELP % {"target": "de l'UE", "objs": "modules", "ordre": ""},
]
el = sco_compute_moy.formsemestre_ue_computation_expr_list(
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
)
if el:
initvalues = el[0]
else:
initvalues = {}
form = [
("ue_id", {"input_type": "hidden"}),
("formsemestre_id", {"input_type": "hidden"}),
(
"computation_expr",
{
"title": "Formule de calcul",
"input_type": "textarea",
"rows": 4,
"cols": 60,
"explanation": "formule de calcul (expérimental)",
},
),
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
form,
submitlabel="Modifier formule de calcul",
cancelbutton="Annuler",
initvalues=initvalues,
)
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
"formsemestre_status?formsemestre_id=" + str(formsemestre_id)
)
else:
if el:
el[0]["computation_expr"] = tf[2]["computation_expr"]
sco_compute_moy.formsemestre_ue_computation_expr_edit(cnx, el[0])
else:
sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, tf[2])
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > modif regle calcul
return flask.redirect(
"formsemestre_status?formsemestre_id="
+ str(formsemestre_id)
+ "&head_message=règle%20de%20calcul%20modifiée"
expr = FormSemestreUEComputationExpr.query.filter_by(
formsemestre_id=formsemestre_id, ue_id=ue_id
).first()
if expr is not None:
db.session.delete(expr)
db.session.commit()
flash("formule supprimée")
return flask.redirect(
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
head_message="formule supprimée",
)
)
@bp.route("/formsemestre_enseignants_list")

View File

@ -269,7 +269,6 @@ def create_user_form(user_name=None, edit=0, all_roles=False):
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}),