forked from ScoDoc/ScoDoc
416 lines
15 KiB
Python
416 lines
15 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
#
|
|
##############################################################################
|
|
|
|
"""Calcul des moyennes de module
|
|
"""
|
|
import pprint
|
|
import traceback
|
|
|
|
from flask import url_for, g
|
|
import app.scodoc.sco_utils as scu
|
|
import app.scodoc.notesdb as ndb
|
|
from app.scodoc.sco_utils import (
|
|
ModuleType,
|
|
NOTES_ATTENTE,
|
|
NOTES_NEUTRALISE,
|
|
EVALUATION_NORMALE,
|
|
EVALUATION_RATTRAPAGE,
|
|
EVALUATION_SESSION2,
|
|
)
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app import log
|
|
from app.scodoc import sco_abs
|
|
from app.scodoc import sco_edit_module
|
|
from app.scodoc import sco_evaluation_db
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_formsemestre_inscriptions
|
|
from app.scodoc import sco_formulas
|
|
from app.scodoc import sco_moduleimpl
|
|
from app.scodoc import sco_etud
|
|
|
|
|
|
def moduleimpl_has_expression(mod):
|
|
"True if we should use a user-defined expression"
|
|
expr = mod["computation_expr"]
|
|
if not expr:
|
|
return False
|
|
expr = expr.strip()
|
|
if not expr or expr[0] == "#":
|
|
return False
|
|
return True
|
|
|
|
|
|
def formsemestre_expressions_use_abscounts(formsemestre_id):
|
|
"""True si les notes de ce semestre dépendent des compteurs d'absences.
|
|
Cela n'est normalement pas le cas, sauf si des formules utilisateur
|
|
utilisent ces compteurs.
|
|
"""
|
|
# check presence of 'nbabs' in expressions
|
|
ab = "nb_abs" # chaine recherchée
|
|
cnx = ndb.GetDBConnexion()
|
|
# 1- moyennes d'UE:
|
|
elist = formsemestre_ue_computation_expr_list(
|
|
cnx, {"formsemestre_id": formsemestre_id}
|
|
)
|
|
for e in elist:
|
|
expr = e["computation_expr"].strip()
|
|
if expr and expr[0] != "#" and ab in expr:
|
|
return True
|
|
# 2- moyennes de modules
|
|
for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
|
|
if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
|
|
return True
|
|
return False
|
|
|
|
|
|
_formsemestre_ue_computation_exprEditor = ndb.EditableTable(
|
|
"notes_formsemestre_ue_computation_expr",
|
|
"notes_formsemestre_ue_computation_expr_id",
|
|
(
|
|
"notes_formsemestre_ue_computation_expr_id",
|
|
"formsemestre_id",
|
|
"ue_id",
|
|
"computation_expr",
|
|
),
|
|
html_quote=False, # does nt automatically quote
|
|
)
|
|
formsemestre_ue_computation_expr_create = _formsemestre_ue_computation_exprEditor.create
|
|
formsemestre_ue_computation_expr_delete = _formsemestre_ue_computation_exprEditor.delete
|
|
formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.list
|
|
formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
|
|
|
|
|
|
def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
|
|
"""Returns UE expression (formula), or None if no expression has been defined"""
|
|
cnx = ndb.GetDBConnexion()
|
|
el = formsemestre_ue_computation_expr_list(
|
|
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
|
|
)
|
|
if not el:
|
|
return None
|
|
else:
|
|
expr = el[0]["computation_expr"].strip()
|
|
if expr and expr[0] != "#":
|
|
if html_quote:
|
|
expr = ndb.quote_html(expr)
|
|
return expr
|
|
else:
|
|
return None
|
|
|
|
|
|
def compute_user_formula(
|
|
sem,
|
|
etudid,
|
|
moy,
|
|
moy_valid,
|
|
notes,
|
|
coefs,
|
|
coefs_mask,
|
|
formula,
|
|
diag_info=None, # infos supplementaires a placer ds messages d'erreur
|
|
use_abs=True,
|
|
):
|
|
"""Calcul moyenne a partir des notes et coefs, en utilisant la formule utilisateur (une chaine).
|
|
Retourne moy, et en cas d'erreur met à jour diag_info (msg)
|
|
"""
|
|
if use_abs:
|
|
nbabs, nbabs_just = sco_abs.get_abs_count(etudid, sem)
|
|
else:
|
|
nbabs, nbabs_just = 0, 0
|
|
try:
|
|
moy_val = float(moy)
|
|
except ValueError:
|
|
moy_val = 0.0 # 0. when no valid value
|
|
variables = {
|
|
"cmask": coefs_mask, # NoteVector(v=coefs_mask),
|
|
"notes": notes, # NoteVector(v=notes),
|
|
"coefs": coefs, # NoteVector(v=coefs),
|
|
"moy": moy,
|
|
"moy_valid": moy_valid, # deprecated, use moy_is_valid
|
|
"moy_is_valid": moy_valid, # True si moyenne numerique
|
|
"moy_val": moy_val,
|
|
"nb_abs": float(nbabs),
|
|
"nb_abs_just": float(nbabs_just),
|
|
"nb_abs_nojust": float(nbabs - nbabs_just),
|
|
}
|
|
try:
|
|
formula = formula.replace("\n", "").replace("\r", "")
|
|
# log('expression : %s\nvariables=%s\n' % (formula, variables)) # debug
|
|
user_moy = sco_formulas.eval_user_expression(formula, variables)
|
|
# log('user_moy=%s' % user_moy)
|
|
if user_moy != "NA":
|
|
user_moy = float(user_moy)
|
|
if (user_moy > 20) or (user_moy < 0):
|
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
|
|
raise ScoValueError(
|
|
f"""
|
|
Valeur moyenne {user_moy} hors limite pour
|
|
<a href="{url_for('notes.formsemestre_bulletinetud',
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=sem["formsemestre_id"],
|
|
etudid=etudid
|
|
)}">{etud["nomprenom"]}</a>"""
|
|
)
|
|
except:
|
|
log(
|
|
"invalid expression : %s\nvariables=%s\n"
|
|
% (formula, pprint.pformat(variables))
|
|
)
|
|
tb = traceback.format_exc()
|
|
log("Exception during evaluation:\n%s\n" % tb)
|
|
diag_info.update({"msg": tb.splitlines()[-1]})
|
|
user_moy = "ERR"
|
|
|
|
# log('formula=%s\nvariables=%s\nmoy=%s\nuser_moy=%s' % (formula, variables, moy, user_moy))
|
|
|
|
return user_moy
|
|
|
|
|
|
# XXX OBSOLETE
|
|
def compute_moduleimpl_moyennes(nt, modimpl):
|
|
"""Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
|
|
au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
|
|
ou en attente), et att (vrai s'il y a des notes en attente dans ce module).
|
|
La moyenne est calculée en utilisant les coefs des évaluations.
|
|
Les notes NEUTRES (abs. excuses) ne sont pas prises en compte.
|
|
Les notes ABS sont remplacées par des zéros.
|
|
S'il manque des notes et que le coef n'est pas nul,
|
|
la moyenne n'est pas calculée: NA
|
|
Ne prend en compte que les evaluations où toutes les notes sont entrées.
|
|
Le résultat note_moyenne est une note sur 20.
|
|
"""
|
|
diag_info = {} # message d'erreur formule
|
|
moduleimpl_id = modimpl["moduleimpl_id"]
|
|
is_malus = modimpl["module"]["module_type"] == ModuleType.MALUS
|
|
sem = sco_formsemestre.get_formsemestre(modimpl["formsemestre_id"])
|
|
etudids = sco_moduleimpl.moduleimpl_listeetuds(
|
|
moduleimpl_id
|
|
) # tous, y compris demissions
|
|
# Inscrits au semestre (pour traiter les demissions):
|
|
inssem_set = set(
|
|
[
|
|
x["etudid"]
|
|
for x in sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
|
modimpl["formsemestre_id"]
|
|
)
|
|
]
|
|
)
|
|
insmod_set = inssem_set.intersection(etudids) # inscrits au semestre et au module
|
|
|
|
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
|
|
evals.sort(
|
|
key=lambda x: (x["numero"], x["jour"], x["heure_debut"])
|
|
) # la plus ancienne en tête
|
|
|
|
user_expr = moduleimpl_has_expression(modimpl)
|
|
attente = False
|
|
# récupere les notes de toutes les evaluations
|
|
eval_rattr = None
|
|
for e in evals:
|
|
e["nb_inscrits"] = e["etat"]["nb_inscrits"]
|
|
# XXX OBSOLETE
|
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
|
e["evaluation_id"]
|
|
) # toutes, y compris demissions
|
|
# restreint aux étudiants encore inscrits à ce module
|
|
notes = [
|
|
notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set)
|
|
]
|
|
e["nb_notes"] = len(notes)
|
|
e["nb_abs"] = len([x for x in notes if x is None])
|
|
e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
|
|
e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
|
|
e["notes"] = notes_db
|
|
|
|
if e["etat"]["evalattente"]:
|
|
attente = True
|
|
if (
|
|
e["evaluation_type"] == EVALUATION_RATTRAPAGE
|
|
or e["evaluation_type"] == EVALUATION_SESSION2
|
|
):
|
|
if eval_rattr:
|
|
# !!! plusieurs rattrapages !
|
|
diag_info.update(
|
|
{
|
|
"msg": "plusieurs évaluations de rattrapage !",
|
|
"moduleimpl_id": moduleimpl_id,
|
|
}
|
|
)
|
|
eval_rattr = e
|
|
|
|
# Les modules MALUS ne sont jamais considérés en attente
|
|
if is_malus:
|
|
attente = False
|
|
|
|
# filtre les evals valides (toutes les notes entrées)
|
|
valid_evals = [
|
|
e
|
|
for e in evals
|
|
if (
|
|
(e["etat"]["evalcomplete"] or e["etat"]["evalattente"])
|
|
and (e["note_max"] > 0)
|
|
)
|
|
]
|
|
#
|
|
R = {}
|
|
formula = scu.unescape_html(modimpl["computation_expr"])
|
|
formula_use_abs = "abs" in formula
|
|
|
|
for etudid in insmod_set: # inscrits au semestre et au module
|
|
sum_notes = 0.0
|
|
sum_coefs = 0.0
|
|
nb_missing = 0
|
|
for e in valid_evals:
|
|
if e["evaluation_type"] != EVALUATION_NORMALE:
|
|
continue
|
|
if etudid in e["notes"]:
|
|
note = e["notes"][etudid]["value"]
|
|
if note is None: # ABSENT
|
|
note = 0
|
|
if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
|
|
sum_notes += (note * 20.0 / e["note_max"]) * e["coefficient"]
|
|
sum_coefs += e["coefficient"]
|
|
else:
|
|
# il manque une note ! (si publish_incomplete, cela peut arriver, on ignore)
|
|
if e["coefficient"] > 0 and not e["publish_incomplete"]:
|
|
nb_missing += 1
|
|
# ne devrait pas arriver ?
|
|
log("\nXXX SCM298\n")
|
|
if nb_missing == 0 and sum_coefs > 0:
|
|
if sum_coefs > 0:
|
|
R[etudid] = sum_notes / sum_coefs
|
|
moy_valid = True
|
|
else:
|
|
R[etudid] = "NA"
|
|
moy_valid = False
|
|
else:
|
|
R[etudid] = "NA"
|
|
moy_valid = False
|
|
|
|
if user_expr:
|
|
# recalcule la moyenne en utilisant la formule utilisateur
|
|
notes = []
|
|
coefs = []
|
|
coefs_mask = [] # 0/1, 0 si coef a ete annulé
|
|
nb_notes = 0 # nombre de notes valides
|
|
for e in evals:
|
|
if (
|
|
(e["etat"]["evalcomplete"] or e["etat"]["evalattente"])
|
|
and etudid in e["notes"]
|
|
) and (e["note_max"] > 0):
|
|
note = e["notes"][etudid]["value"]
|
|
if note is None:
|
|
note = 0
|
|
if note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
|
|
notes.append(note * 20.0 / e["note_max"])
|
|
coefs.append(e["coefficient"])
|
|
coefs_mask.append(1)
|
|
nb_notes += 1
|
|
else:
|
|
notes.append(0.0)
|
|
coefs.append(0.0)
|
|
coefs_mask.append(0)
|
|
else:
|
|
notes.append(0.0)
|
|
coefs.append(0.0)
|
|
coefs_mask.append(0)
|
|
if nb_notes > 0 or formula_use_abs:
|
|
user_moy = compute_user_formula(
|
|
sem,
|
|
etudid,
|
|
R[etudid],
|
|
moy_valid,
|
|
notes,
|
|
coefs,
|
|
coefs_mask,
|
|
formula,
|
|
diag_info=diag_info,
|
|
use_abs=formula_use_abs,
|
|
)
|
|
if diag_info:
|
|
diag_info["moduleimpl_id"] = moduleimpl_id
|
|
R[etudid] = user_moy
|
|
# Note de rattrapage ou deuxième session ?
|
|
if eval_rattr:
|
|
if etudid in eval_rattr["notes"]:
|
|
note = eval_rattr["notes"][etudid]["value"]
|
|
if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
|
|
if not isinstance(R[etudid], float):
|
|
R[etudid] = note
|
|
else:
|
|
note_sur_20 = note * 20.0 / eval_rattr["note_max"]
|
|
if eval_rattr["evaluation_type"] == EVALUATION_RATTRAPAGE:
|
|
# rattrapage classique: prend la meilleure note entre moyenne
|
|
# module et note eval rattrapage
|
|
if (R[etudid] == "NA") or (note_sur_20 > R[etudid]):
|
|
# log('note_sur_20=%s' % note_sur_20)
|
|
R[etudid] = note_sur_20
|
|
elif eval_rattr["evaluation_type"] == EVALUATION_SESSION2:
|
|
# rattrapage type "deuxième session": remplace la note moyenne
|
|
R[etudid] = note_sur_20
|
|
|
|
return R, valid_evals, attente, diag_info
|
|
|
|
|
|
def formsemestre_compute_modimpls_moyennes(nt, formsemestre_id):
|
|
"""retourne dict { moduleimpl_id : { etudid, note_moyenne_dans_ce_module } },
|
|
la liste des moduleimpls, la liste des evaluations valides,
|
|
liste des moduleimpls avec notes en attente.
|
|
"""
|
|
# sem = sco_formsemestre.get_formsemestre( formsemestre_id)
|
|
# inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
|
# args={"formsemestre_id": formsemestre_id}
|
|
# )
|
|
# etudids = [x["etudid"] for x in inscr]
|
|
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
|
# recupere les moyennes des etudiants de tous les modules
|
|
D = {}
|
|
valid_evals = []
|
|
valid_evals_per_mod = {} # { moduleimpl_id : eval }
|
|
mods_att = []
|
|
expr_diags = []
|
|
for modimpl in modimpls:
|
|
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
|
|
modimpl["module"] = mod # add module dict to moduleimpl (used by nt)
|
|
moduleimpl_id = modimpl["moduleimpl_id"]
|
|
assert moduleimpl_id not in D
|
|
(
|
|
D[moduleimpl_id],
|
|
valid_evals_mod,
|
|
attente,
|
|
expr_diag,
|
|
) = compute_moduleimpl_moyennes(nt, modimpl)
|
|
valid_evals_per_mod[moduleimpl_id] = valid_evals_mod
|
|
valid_evals += valid_evals_mod
|
|
if attente:
|
|
mods_att.append(modimpl)
|
|
if expr_diag:
|
|
expr_diags.append(expr_diag)
|
|
#
|
|
return D, modimpls, valid_evals_per_mod, valid_evals, mods_att, expr_diags
|