forked from ScoDoc/ScoDoc
1357 lines
54 KiB
Python
1357 lines
54 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Calculs sur les notes et cache des résultats
|
|
|
|
Ancien code ScoDoc 7 en cours de rénovation
|
|
"""
|
|
|
|
from operator import itemgetter
|
|
|
|
from flask import g, url_for
|
|
|
|
from app.but import bulletin_but
|
|
from app.models import FormSemestre, Identite
|
|
from app.models import ScoDocSiteConfig
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc.sco_utils import ModuleType
|
|
import app.scodoc.notesdb as ndb
|
|
from app import log
|
|
from app.scodoc.sco_formulas import NoteVector
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
|
|
from app.scodoc.sco_formsemestre import (
|
|
formsemestre_uecoef_list,
|
|
formsemestre_uecoef_create,
|
|
)
|
|
from app.scodoc.sco_codes_parcours import (
|
|
DEF,
|
|
UE_SPORT,
|
|
ue_is_fondamentale,
|
|
ue_is_professionnelle,
|
|
)
|
|
from app.scodoc import sco_cache
|
|
from app.scodoc import sco_codes_parcours
|
|
from app.scodoc import sco_compute_moy
|
|
from app.scodoc.sco_cursus import formsemestre_get_etud_capitalisation
|
|
from app.scodoc import sco_cursus_dut
|
|
from app.scodoc import sco_edit_matiere
|
|
from app.scodoc import sco_edit_module
|
|
from app.scodoc import sco_edit_ue
|
|
from app.scodoc import sco_etud
|
|
from app.scodoc import sco_evaluations
|
|
from app.scodoc import sco_formations
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_formsemestre_inscriptions
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_moduleimpl
|
|
from app.scodoc import sco_preferences
|
|
|
|
|
|
def comp_ranks(T):
|
|
"""Calcul rangs à partir d'une liste ordonnée de tuples [ (valeur, ..., etudid) ]
|
|
(valeur est une note numérique), en tenant compte des ex-aequos
|
|
Le resultat est: { etudid : rang } où rang est une chaine decrivant le rang
|
|
"""
|
|
rangs = {} # { etudid : rang } (rang est une chaine)
|
|
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
|
|
for i in range(len(T)):
|
|
# test ex-aequo
|
|
if i < len(T) - 1:
|
|
next = T[i + 1][0]
|
|
else:
|
|
next = None
|
|
moy = T[i][0]
|
|
if nb_ex:
|
|
srang = "%d ex" % (i + 1 - nb_ex)
|
|
if moy == next:
|
|
nb_ex += 1
|
|
else:
|
|
nb_ex = 0
|
|
else:
|
|
if moy == next:
|
|
srang = "%d ex" % (i + 1 - nb_ex)
|
|
nb_ex = 1
|
|
else:
|
|
srang = "%d" % (i + 1)
|
|
rangs[T[i][-1]] = srang # str(i+1)
|
|
return rangs
|
|
|
|
|
|
def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
|
|
"""Get liste des UE du semestre (à partir des moduleimpls)
|
|
(utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
|
|
"""
|
|
if modimpls is None:
|
|
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
|
uedict = {}
|
|
for modimpl in modimpls:
|
|
mod = sco_edit_module.module_list(args={"module_id": modimpl["module_id"]})[0]
|
|
modimpl["module"] = mod
|
|
if not mod["ue_id"] in uedict:
|
|
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
|
|
uedict[ue["ue_id"]] = ue
|
|
ues = list(uedict.values())
|
|
ues.sort(key=lambda u: u["numero"])
|
|
return ues, modimpls
|
|
|
|
|
|
def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
|
|
"""Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
|
|
ou None s'il n'y a aucun module.
|
|
|
|
(nécessaire pour éviter appels récursifs de nt, qui peuvent boucler)
|
|
"""
|
|
infos = ndb.SimpleDictFetch(
|
|
"""SELECT mod.coefficient
|
|
FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
|
|
WHERE mod.id = mi.module_id
|
|
and ins.etudid = %(etudid)s
|
|
and ins.moduleimpl_id = mi.id
|
|
and mi.formsemestre_id = %(formsemestre_id)s
|
|
and mod.ue_id = %(ue_id)s
|
|
""",
|
|
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
|
|
)
|
|
|
|
if not infos:
|
|
return None
|
|
else:
|
|
s = sum(x["coefficient"] for x in infos)
|
|
return s
|
|
|
|
|
|
class NotesTable:
|
|
"""Une NotesTable représente un tableau de notes pour un semestre de formation.
|
|
Les colonnes sont des modules.
|
|
Les lignes des étudiants.
|
|
On peut calculer les moyennes par étudiant (pondérées par les coefs)
|
|
ou les moyennes par module.
|
|
|
|
Attributs publics (en lecture):
|
|
- inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
|
|
- identdict: { etudid : ident }
|
|
- sem : le formsemestre
|
|
get_table_moyennes_triees: [ (moy_gen, moy_ue1, moy_ue2, ... moy_ues, moy_mod1, ..., moy_modn, etudid) ]
|
|
(où toutes les valeurs sont soit des nombres soit des chaines spéciales comme 'NA', 'NI'),
|
|
incluant les UE de sport
|
|
|
|
- bonus[etudid] : valeur du bonus "sport".
|
|
|
|
Attributs privés:
|
|
- _modmoys : { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
|
|
- _ues : liste des UE de ce semestre (hors capitalisees)
|
|
- _matmoys : { matiere_id : { etudid: note moyenne dans cette matiere } }
|
|
|
|
"""
|
|
|
|
def __init__(self, formsemestre_id):
|
|
# log(f"NotesTable( formsemestre_id={formsemestre_id} )")
|
|
raise NotImplementedError() # XXX
|
|
if not formsemestre_id:
|
|
raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
|
|
self.formsemestre_id = formsemestre_id
|
|
cnx = ndb.GetDBConnexion()
|
|
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
self.moduleimpl_stats = {} # { moduleimpl_id : {stats} }
|
|
self._uecoef = {} # { ue_id : coef } cache coef manuels ue cap
|
|
self._evaluations_etats = None # liste des evaluations avec état
|
|
self.use_ue_coefs = sco_preferences.get_preference(
|
|
"use_ue_coefs", formsemestre_id
|
|
)
|
|
# si vrai, bloque calcul des moy gen. et d'UE.:
|
|
self.block_moyennes = self.sem["block_moyennes"]
|
|
# Infos sur les etudiants
|
|
self.inscrlist = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
|
args={"formsemestre_id": formsemestre_id}
|
|
)
|
|
# infos identite etudiant
|
|
# xxx sous-optimal: 1/select par etudiant -> 0.17" pour identdict sur GTR1 !
|
|
self.identdict = {} # { etudid : ident }
|
|
self.inscrdict = {} # { etudid : inscription }
|
|
for x in self.inscrlist:
|
|
i = sco_etud.etudident_list(cnx, {"etudid": x["etudid"]})[0]
|
|
self.identdict[x["etudid"]] = i
|
|
self.inscrdict[x["etudid"]] = x
|
|
x["nomp"] = (i["nom_usuel"] or i["nom"]) + i["prenom"] # pour tri
|
|
|
|
# Tri les etudids par NOM
|
|
self.inscrlist.sort(key=itemgetter("nomp"))
|
|
|
|
# { etudid : rang dans l'ordre alphabetique }
|
|
self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
|
|
|
|
self.bonus = scu.DictDefault(defaultvalue=0)
|
|
# Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
|
|
(
|
|
self._modmoys,
|
|
self._modimpls,
|
|
self._valid_evals_per_mod,
|
|
valid_evals,
|
|
mods_att,
|
|
self.expr_diagnostics,
|
|
) = sco_compute_moy.formsemestre_compute_modimpls_moyennes(
|
|
self, formsemestre_id
|
|
)
|
|
self._mods_att = mods_att # liste des modules avec des notes en attente
|
|
self._matmoys = {} # moyennes par matieres
|
|
self._valid_evals = {} # { evaluation_id : eval }
|
|
for e in valid_evals:
|
|
self._valid_evals[e["evaluation_id"]] = e # Liste des modules et UE
|
|
uedict = {} # public member: { ue_id : ue }
|
|
self.uedict = uedict # les ues qui ont un modimpl dans ce semestre
|
|
for modimpl in self._modimpls:
|
|
# module has been added by formsemestre_compute_modimpls_moyennes
|
|
mod = modimpl["module"]
|
|
if not mod["ue_id"] in uedict:
|
|
ue = sco_edit_ue.ue_list(args={"ue_id": mod["ue_id"]})[0]
|
|
uedict[ue["ue_id"]] = ue
|
|
else:
|
|
ue = uedict[mod["ue_id"]]
|
|
modimpl["ue"] = ue # add ue dict to moduleimpl
|
|
self._matmoys[mod["matiere_id"]] = {}
|
|
mat = sco_edit_matiere.matiere_list(args={"matiere_id": mod["matiere_id"]})[
|
|
0
|
|
]
|
|
modimpl["mat"] = mat # add matiere dict to moduleimpl
|
|
# calcul moyennes du module et stocke dans le module
|
|
# nb_inscrits, nb_notes, nb_abs, nb_neutre, moy, median, last_modif=
|
|
|
|
self.formation = sco_formations.formation_list(
|
|
args={"formation_id": self.sem["formation_id"]}
|
|
)[0]
|
|
self.parcours = sco_codes_parcours.get_parcours_from_code(
|
|
self.formation["type_parcours"]
|
|
)
|
|
|
|
# En APC, il faut avoir toutes les UE du semestre
|
|
# (elles n'ont pas nécessairement un module rattaché):
|
|
if self.parcours.APC_SAE:
|
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
|
for ue in formsemestre.query_ues():
|
|
if ue.id not in self.uedict:
|
|
self.uedict[ue.id] = ue.to_dict()
|
|
|
|
# Decisions jury et UE capitalisées
|
|
self.comp_decisions_jury()
|
|
self.comp_ue_capitalisees()
|
|
|
|
# Liste des moyennes de tous, en chaines de car., triées
|
|
self._ues = list(uedict.values())
|
|
self._ues.sort(key=lambda u: u["numero"])
|
|
|
|
T = []
|
|
|
|
self.moy_gen = {} # etudid : moy gen (avec UE capitalisées)
|
|
self.moy_ue = {} # ue_id : { etudid : moy ue } (valeur numerique)
|
|
self.etud_moy_infos = {} # etudid : resultats de comp_etud_moy_gen()
|
|
valid_moy = [] # liste des valeurs valides de moyenne generale (pour min/max)
|
|
for ue in self._ues:
|
|
self.moy_ue[ue["ue_id"]] = {}
|
|
self._etud_moy_ues = {} # { etudid : { ue_id : {'moy', 'sum_coefs', ... } }
|
|
|
|
for etudid in self.get_etudids():
|
|
etud_moy_gen = self.comp_etud_moy_gen(etudid, cnx)
|
|
self.etud_moy_infos[etudid] = etud_moy_gen
|
|
ue_status = etud_moy_gen["moy_ues"]
|
|
self._etud_moy_ues[etudid] = ue_status
|
|
|
|
moy_gen = etud_moy_gen["moy"]
|
|
self.moy_gen[etudid] = moy_gen
|
|
if etud_moy_gen["sum_coefs"] > 0:
|
|
valid_moy.append(moy_gen)
|
|
|
|
moy_ues = []
|
|
for ue in self._ues:
|
|
moy_ue = ue_status[ue["ue_id"]]["moy"]
|
|
moy_ues.append(moy_ue)
|
|
self.moy_ue[ue["ue_id"]][etudid] = moy_ue
|
|
|
|
t = [moy_gen] + moy_ues
|
|
#
|
|
is_cap = {} # ue_id : is_capitalized
|
|
for ue in self._ues:
|
|
is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"]
|
|
|
|
for modimpl in self.get_modimpls_dict():
|
|
val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
|
if is_cap[modimpl["module"]["ue_id"]]:
|
|
t.append("-c-")
|
|
else:
|
|
t.append(val)
|
|
#
|
|
t.append(etudid)
|
|
T.append(t)
|
|
|
|
self.T = T
|
|
# tri par moyennes décroissantes,
|
|
# en laissant les demissionnaires a la fin, par ordre alphabetique
|
|
self.T.sort(key=self._row_key)
|
|
|
|
if len(valid_moy):
|
|
self.moy_min = min(valid_moy)
|
|
self.moy_max = max(valid_moy)
|
|
else:
|
|
self.moy_min = self.moy_max = "NA"
|
|
|
|
# calcul rangs (/ moyenne generale)
|
|
self.etud_moy_gen_ranks = comp_ranks(T)
|
|
|
|
self.rangs_groupes = (
|
|
{}
|
|
) # { group_id : { etudid : rang } } (lazy, see get_etud_rang_group)
|
|
self.group_etuds = (
|
|
{}
|
|
) # { group_id : set of etudids } (lazy, see get_etud_rang_group)
|
|
|
|
# calcul rangs dans chaque UE
|
|
ue_rangs = (
|
|
{}
|
|
) # ue_rangs[ue_id] = ({ etudid : rang }, nb_inscrits) (rang est une chaine)
|
|
for ue in self._ues:
|
|
ue_id = ue["ue_id"]
|
|
val_ids = [
|
|
(self.moy_ue[ue_id][etudid], etudid) for etudid in self.moy_ue[ue_id]
|
|
]
|
|
ue_eff = len(
|
|
[x for x in val_ids if isinstance(x[0], float)]
|
|
) # nombre d'étudiants avec une note dans l'UE
|
|
val_ids.sort(key=self._row_key)
|
|
ue_rangs[ue_id] = (
|
|
comp_ranks(val_ids),
|
|
ue_eff,
|
|
) # et non: len(self.moy_ue[ue_id]) qui est l'effectif de la promo
|
|
self.ue_rangs = ue_rangs
|
|
# ---- calcul rangs dans les modules
|
|
self.mod_rangs = {}
|
|
for modimpl in self._modimpls:
|
|
vals = self._modmoys[modimpl["moduleimpl_id"]]
|
|
val_ids = [(vals[etudid], etudid) for etudid in vals.keys()]
|
|
val_ids.sort(key=self._row_key)
|
|
self.mod_rangs[modimpl["moduleimpl_id"]] = (comp_ranks(val_ids), len(vals))
|
|
#
|
|
self.compute_moy_moy()
|
|
#
|
|
log(f"NotesTable( formsemestre_id={formsemestre_id} ) done.")
|
|
|
|
def _row_key(self, x):
|
|
"""clé de tri par moyennes décroissantes,
|
|
en laissant les demissionnaires a la fin, par ordre alphabetique.
|
|
(moy_gen, rang_alpha)
|
|
"""
|
|
try:
|
|
moy = -float(x[0])
|
|
except (ValueError, TypeError):
|
|
moy = 1000.0
|
|
return (moy, self._rang_alpha[x[-1]])
|
|
|
|
def get_etudids(self, sorted=False):
|
|
if sorted:
|
|
# Tri par moy. generale décroissante
|
|
return [x[-1] for x in self.T]
|
|
else:
|
|
# Tri par ordre alphabetique de NOM
|
|
return [x["etudid"] for x in self.inscrlist]
|
|
|
|
def get_sexnom(self, etudid):
|
|
"M. DUPONT"
|
|
etud = self.identdict[etudid]
|
|
return etud["civilite_str"] + " " + (etud["nom_usuel"] or etud["nom"]).upper()
|
|
|
|
def get_nom_short(self, etudid):
|
|
"formatte nom d'un etud (pour table recap)"
|
|
etud = self.identdict[etudid]
|
|
# Attention aux caracteres multibytes pour decouper les 2 premiers:
|
|
return (
|
|
(etud["nom_usuel"] or etud["nom"]).upper()
|
|
+ " "
|
|
+ etud["prenom"].capitalize()[:2]
|
|
+ "."
|
|
)
|
|
|
|
def get_nom_long(self, etudid):
|
|
"formatte nom d'un etud: M. Pierre DUPONT"
|
|
etud = self.identdict[etudid]
|
|
return sco_etud.format_nomprenom(etud)
|
|
|
|
def get_displayed_etud_code(self, etudid):
|
|
'code à afficher sur les listings "anonymes"'
|
|
return self.identdict[etudid]["code_nip"] or self.identdict[etudid]["etudid"]
|
|
|
|
def get_etud_etat(self, etudid):
|
|
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
|
|
if etudid in self.inscrdict:
|
|
return self.inscrdict[etudid]["etat"]
|
|
else:
|
|
return ""
|
|
|
|
def get_etud_etat_html(self, etudid):
|
|
etat = self.inscrdict[etudid]["etat"]
|
|
if etat == scu.INSCRIT:
|
|
return ""
|
|
elif etat == scu.DEMISSION:
|
|
return ' <font color="red">(DEMISSIONNAIRE)</font> '
|
|
elif etat == DEF:
|
|
return ' <font color="red">(DEFAILLANT)</font> '
|
|
else:
|
|
return ' <font color="red">(%s)</font> ' % etat
|
|
|
|
def get_ues_stat_dict(self, filter_sport=False): # was get_ues()
|
|
"""Liste des UEs, ordonnée par numero.
|
|
Si filter_sport, retire les UE de type SPORT
|
|
"""
|
|
if not filter_sport:
|
|
return self._ues
|
|
else:
|
|
return [ue for ue in self._ues if ue["type"] != UE_SPORT]
|
|
|
|
def get_modimpls_dict(self, ue_id=None):
|
|
"Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
|
|
if ue_id is None:
|
|
r = self._modimpls
|
|
else:
|
|
r = [m for m in self._modimpls if m["ue"]["ue_id"] == ue_id]
|
|
# trie la liste par ue.numero puis mat.numero puis mod.numero
|
|
r.sort(
|
|
key=lambda x: (x["ue"]["numero"], x["mat"]["numero"], x["module"]["numero"])
|
|
)
|
|
return r
|
|
|
|
def get_etud_eval_note(self, etudid, evaluation_id):
|
|
"note d'un etudiant a une evaluation"
|
|
return self._valid_evals[evaluation_id]["notes"][etudid]
|
|
|
|
def get_evals_in_mod(self, moduleimpl_id):
|
|
"liste des evaluations valides dans un module"
|
|
return [
|
|
e for e in self._valid_evals.values() if e["moduleimpl_id"] == moduleimpl_id
|
|
]
|
|
|
|
def get_mod_stats(self, moduleimpl_id):
|
|
"""moyenne generale, min, max pour un module
|
|
Ne prend en compte que les evaluations où toutes les notes sont entrées
|
|
Cache le resultat.
|
|
"""
|
|
if moduleimpl_id in self.moduleimpl_stats:
|
|
return self.moduleimpl_stats[moduleimpl_id]
|
|
nb_notes = 0
|
|
sum_notes = 0.0
|
|
nb_missing = 0
|
|
moys = self._modmoys[moduleimpl_id]
|
|
vals = []
|
|
for etudid in self.get_etudids():
|
|
# saute les demissionnaires et les défaillants:
|
|
if self.inscrdict[etudid]["etat"] != scu.INSCRIT:
|
|
continue
|
|
val = moys.get(etudid, None) # None si non inscrit
|
|
try:
|
|
vals.append(float(val))
|
|
except:
|
|
nb_missing = nb_missing + 1
|
|
sum_notes = sum(vals)
|
|
nb_notes = len(vals)
|
|
if nb_notes > 0:
|
|
moy = sum_notes / nb_notes
|
|
max_note, min_note = max(vals), min(vals)
|
|
else:
|
|
moy, min_note, max_note = "NA", "-", "-"
|
|
s = {
|
|
"moy": moy,
|
|
"max": max_note,
|
|
"min": min_note,
|
|
"nb_notes": nb_notes,
|
|
"nb_missing": nb_missing,
|
|
"nb_valid_evals": len(self._valid_evals_per_mod[moduleimpl_id]),
|
|
}
|
|
self.moduleimpl_stats[moduleimpl_id] = s
|
|
return s
|
|
|
|
def compute_moy_moy(self):
|
|
"""precalcule les moyennes d'UE et generale (moyennes sur tous
|
|
les etudiants), et les stocke dans self.moy_moy, self.ue['moy']
|
|
|
|
Les moyennes d'UE ne tiennent pas compte des capitalisations.
|
|
"""
|
|
ues = self.get_ues_stat_dict()
|
|
sum_moy = 0 # la somme des moyennes générales valides
|
|
nb_moy = 0 # le nombre de moyennes générales valides
|
|
for ue in ues:
|
|
ue["_notes"] = [] # liste tmp des valeurs de notes valides dans l'ue
|
|
nb_dem = 0 # nb d'étudiants démissionnaires dans le semestre
|
|
nb_def = 0 # nb d'étudiants défaillants dans le semestre
|
|
T = self.get_table_moyennes_triees()
|
|
for t in T:
|
|
etudid = t[-1]
|
|
# saute les demissionnaires et les défaillants:
|
|
if self.inscrdict[etudid]["etat"] != scu.INSCRIT:
|
|
if self.inscrdict[etudid]["etat"] == scu.DEMISSION:
|
|
nb_dem += 1
|
|
if self.inscrdict[etudid]["etat"] == DEF:
|
|
nb_def += 1
|
|
continue
|
|
try:
|
|
sum_moy += float(t[0])
|
|
nb_moy += 1
|
|
except:
|
|
pass
|
|
i = 0
|
|
for ue in ues:
|
|
i += 1
|
|
try:
|
|
ue["_notes"].append(float(t[i]))
|
|
except:
|
|
pass
|
|
self.nb_demissions = nb_dem
|
|
self.nb_defaillants = nb_def
|
|
if nb_moy > 0:
|
|
self.moy_moy = sum_moy / nb_moy
|
|
else:
|
|
self.moy_moy = "-"
|
|
|
|
i = 0
|
|
for ue in ues:
|
|
i += 1
|
|
ue["nb_vals"] = len(ue["_notes"])
|
|
if ue["nb_vals"] > 0:
|
|
ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"]
|
|
ue["max"] = max(ue["_notes"])
|
|
ue["min"] = min(ue["_notes"])
|
|
else:
|
|
ue["moy"], ue["max"], ue["min"] = "", "", ""
|
|
del ue["_notes"]
|
|
|
|
def get_etud_mod_moy(self, moduleimpl_id, etudid):
|
|
"""moyenne d'un etudiant dans un module (ou NI si non inscrit)"""
|
|
return self._modmoys[moduleimpl_id].get(etudid, "NI")
|
|
|
|
def get_etud_mat_moy(self, matiere_id, etudid):
|
|
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
|
|
matmoy = self._matmoys.get(matiere_id, None)
|
|
if not matmoy:
|
|
return "NM" # non inscrit
|
|
# log('*** oups: get_etud_mat_moy(%s, %s)' % (matiere_id, etudid))
|
|
# raise ValueError('matiere invalide !') # should not occur
|
|
return matmoy.get(etudid, "NA")
|
|
|
|
def comp_etud_moy_ue(self, etudid, ue_id=None, cnx=None):
|
|
"""Calcule moyenne gen. pour un etudiant dans une UE
|
|
Ne prend en compte que les evaluations où toutes les notes sont entrées
|
|
Return a dict(moy, nb_notes, nb_missing, sum_coefs)
|
|
Si pas de notes, moy == 'NA' et sum_coefs==0
|
|
Si non inscrit, moy == 'NI' et sum_coefs==0
|
|
"""
|
|
assert ue_id
|
|
modimpls = self.get_modimpls_dict(ue_id)
|
|
nb_notes = 0 # dans cette UE
|
|
sum_notes = 0.0
|
|
sum_coefs = 0.0
|
|
nb_missing = 0 # nb de modules sans note dans cette UE
|
|
|
|
notes_bonus_gen = [] # liste des notes de sport et culture
|
|
coefs_bonus_gen = []
|
|
|
|
ue_malus = 0.0 # malus à appliquer à cette moyenne d'UE
|
|
|
|
notes = NoteVector()
|
|
coefs = NoteVector()
|
|
coefs_mask = NoteVector() # 0/1, 0 si coef a ete annulé
|
|
|
|
matiere_id_last = None
|
|
matiere_sum_notes = matiere_sum_coefs = 0.0
|
|
|
|
est_inscrit = False # inscrit à l'un des modules de cette UE ?
|
|
|
|
for modimpl in modimpls:
|
|
# module ne faisant pas partie d'une UE capitalisee
|
|
val = self._modmoys[modimpl["moduleimpl_id"]].get(etudid, "NI")
|
|
# si 'NI', etudiant non inscrit a ce module
|
|
if val != "NI":
|
|
est_inscrit = True
|
|
if modimpl["module"]["module_type"] == ModuleType.STANDARD:
|
|
coef = modimpl["module"]["coefficient"]
|
|
if modimpl["ue"]["type"] != UE_SPORT:
|
|
notes.append(val, name=modimpl["module"]["code"])
|
|
try:
|
|
sum_notes += val * coef
|
|
sum_coefs += coef
|
|
nb_notes = nb_notes + 1
|
|
coefs.append(coef)
|
|
coefs_mask.append(1)
|
|
matiere_id = modimpl["module"]["matiere_id"]
|
|
if (
|
|
matiere_id_last
|
|
and matiere_id != matiere_id_last
|
|
and matiere_sum_coefs
|
|
):
|
|
self._matmoys[matiere_id_last][etudid] = (
|
|
matiere_sum_notes / matiere_sum_coefs
|
|
)
|
|
matiere_sum_notes = matiere_sum_coefs = 0.0
|
|
matiere_sum_notes += val * coef
|
|
matiere_sum_coefs += coef
|
|
matiere_id_last = matiere_id
|
|
except TypeError: # val == "NI" "NA"
|
|
assert val == "NI" or val == "NA" or val == "ERR"
|
|
nb_missing = nb_missing + 1
|
|
coefs.append(0)
|
|
coefs_mask.append(0)
|
|
|
|
else: # UE_SPORT:
|
|
# la note du module de sport agit directement sur la moyenne gen.
|
|
try:
|
|
notes_bonus_gen.append(float(val))
|
|
coefs_bonus_gen.append(coef)
|
|
except:
|
|
# log('comp_etud_moy_ue: exception: val=%s coef=%s' % (val,coef))
|
|
pass
|
|
elif modimpl["module"]["module_type"] == ModuleType.MALUS:
|
|
try:
|
|
ue_malus += val
|
|
except:
|
|
pass # si non inscrit ou manquant, ignore
|
|
elif modimpl["module"]["module_type"] in (
|
|
ModuleType.RESSOURCE,
|
|
ModuleType.SAE,
|
|
):
|
|
# XXX temporaire pour ne pas bloquer durant le dev
|
|
pass
|
|
else:
|
|
raise ValueError(
|
|
"invalid module type (%s)" % modimpl["module"]["module_type"]
|
|
)
|
|
|
|
if matiere_id_last and matiere_sum_coefs:
|
|
self._matmoys[matiere_id_last][etudid] = (
|
|
matiere_sum_notes / matiere_sum_coefs
|
|
)
|
|
|
|
# Calcul moyenne:
|
|
if sum_coefs > 0:
|
|
moy = sum_notes / sum_coefs
|
|
if ue_malus:
|
|
moy -= ue_malus
|
|
moy = max(scu.NOTES_MIN, min(moy, 20.0))
|
|
moy_valid = True
|
|
else:
|
|
moy = "NA"
|
|
moy_valid = False
|
|
|
|
# Recalcule la moyenne en utilisant une formule utilisateur
|
|
expr_diag = {}
|
|
formula = sco_compute_moy.get_ue_expression(self.formsemestre_id, ue_id)
|
|
if formula:
|
|
moy = sco_compute_moy.compute_user_formula(
|
|
self.sem,
|
|
etudid,
|
|
moy,
|
|
moy_valid,
|
|
notes,
|
|
coefs,
|
|
coefs_mask,
|
|
formula,
|
|
diag_info=expr_diag,
|
|
)
|
|
if expr_diag:
|
|
expr_diag["ue_id"] = ue_id
|
|
self.expr_diagnostics.append(expr_diag)
|
|
|
|
return dict(
|
|
moy=moy,
|
|
nb_notes=nb_notes,
|
|
nb_missing=nb_missing,
|
|
sum_coefs=sum_coefs,
|
|
notes_bonus_gen=notes_bonus_gen,
|
|
coefs_bonus_gen=coefs_bonus_gen,
|
|
expr_diag=expr_diag,
|
|
ue_malus=ue_malus,
|
|
est_inscrit=est_inscrit,
|
|
)
|
|
|
|
def comp_etud_moy_gen(self, etudid, cnx):
|
|
"""Calcule moyenne gen. pour un etudiant
|
|
Return a dict:
|
|
moy : moyenne générale
|
|
nb_notes, nb_missing, sum_coefs
|
|
ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
|
|
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
|
|
ects_pot_pro: (float) nb d'ECTS issus d'UE pro
|
|
moy_ues : { ue_id : ue_status }
|
|
où ue_status = {
|
|
'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
|
|
'moy' : moyenne, avec capitalisation eventuelle
|
|
'capitalized_ue_id' : id de l'UE capitalisée
|
|
'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
|
|
(la somme des coefs des modules, ou le coef d'UE capitalisée,
|
|
ou encore le coef d'UE si l'option use_ue_coefs est active)
|
|
'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
|
|
'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
|
|
'is_capitalized' : True|False,
|
|
'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
|
|
'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,
|
|
'ects_pot_pro' : 0 si UE non pro, = ects_pot sinon,
|
|
'formsemestre_id' : (si capitalisee),
|
|
'event_date' : (si capitalisee)
|
|
}
|
|
Si pas de notes, moy == 'NA' et sum_coefs==0
|
|
|
|
Prend toujours en compte les UE capitalisées.
|
|
"""
|
|
# Si l'étudiant a Démissionné ou est DEFaillant, on n'enregistre pas ses moyennes
|
|
block_computation = (
|
|
self.inscrdict[etudid]["etat"] == "D"
|
|
or self.inscrdict[etudid]["etat"] == DEF
|
|
or self.block_moyennes
|
|
)
|
|
|
|
moy_ues = {}
|
|
notes_bonus_gen = (
|
|
[]
|
|
) # liste des notes de sport et culture (s'appliquant à la MG)
|
|
coefs_bonus_gen = []
|
|
nb_notes = 0 # nb de notes d'UE (non capitalisees)
|
|
sum_notes = 0.0 # somme des notes d'UE
|
|
# somme des coefs d'UE (eux-même somme des coefs de modules avec notes):
|
|
sum_coefs = 0.0
|
|
|
|
nb_missing = 0 # nombre d'UE sans notes
|
|
sem_ects_pot = 0.0
|
|
sem_ects_pot_fond = 0.0
|
|
sem_ects_pot_pro = 0.0
|
|
|
|
for ue in self.get_ues_stat_dict():
|
|
# - On calcule la moyenne d'UE courante:
|
|
if not block_computation:
|
|
mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx)
|
|
else:
|
|
mu = dict(
|
|
moy="NA",
|
|
nb_notes=0,
|
|
nb_missing=0,
|
|
sum_coefs=0,
|
|
notes_bonus_gen=0,
|
|
coefs_bonus_gen=0,
|
|
expr_diag="",
|
|
est_inscrit=False,
|
|
)
|
|
# infos supplementaires pouvant servir au calcul du bonus sport
|
|
mu["ue"] = ue
|
|
moy_ues[ue["ue_id"]] = mu
|
|
|
|
# - Faut-il prendre une UE capitalisée ?
|
|
if mu["moy"] != "NA" and mu["est_inscrit"]:
|
|
max_moy_ue = mu["moy"]
|
|
else:
|
|
# pas de notes dans l'UE courante, ou pas inscrit
|
|
max_moy_ue = 0.0
|
|
if not mu["est_inscrit"]:
|
|
coef_ue = 0.0
|
|
else:
|
|
if self.use_ue_coefs:
|
|
coef_ue = mu["ue"]["coefficient"]
|
|
else:
|
|
# coef UE = sum des coefs modules
|
|
coef_ue = mu["sum_coefs"]
|
|
|
|
# is_capitalized si l'UE prise en compte est une UE capitalisée
|
|
mu["is_capitalized"] = False
|
|
# was_capitalized s'il y a precedemment une UE capitalisée (pas forcement meilleure)
|
|
mu["was_capitalized"] = False
|
|
|
|
is_external = False
|
|
event_date = None
|
|
if not block_computation:
|
|
for ue_cap in self.ue_capitalisees[etudid]:
|
|
if ue_cap["ue_code"] == ue["ue_code"]:
|
|
moy_ue_cap = ue_cap["moy"]
|
|
mu["was_capitalized"] = True
|
|
event_date = event_date or ue_cap["event_date"]
|
|
if (
|
|
(moy_ue_cap != "NA")
|
|
and isinstance(moy_ue_cap, float)
|
|
and isinstance(max_moy_ue, float)
|
|
and (moy_ue_cap > max_moy_ue)
|
|
):
|
|
# meilleure UE capitalisée
|
|
event_date = ue_cap["event_date"]
|
|
max_moy_ue = moy_ue_cap
|
|
mu["is_capitalized"] = True
|
|
capitalized_ue_id = ue_cap["ue_id"]
|
|
formsemestre_id = ue_cap["formsemestre_id"]
|
|
coef_ue = self.get_etud_ue_cap_coef(
|
|
etudid, ue, ue_cap, cnx=cnx
|
|
)
|
|
is_external = ue_cap["is_external"]
|
|
|
|
mu["cur_moy_ue"] = mu["moy"] # la moyenne dans le sem. courant
|
|
if mu["est_inscrit"]:
|
|
mu["cur_coef_ue"] = mu["sum_coefs"]
|
|
else:
|
|
mu["cur_coef_ue"] = 0.0
|
|
mu["moy"] = max_moy_ue # la moyenne d'UE a prendre en compte
|
|
mu["is_external"] = is_external # validation externe (dite "antérieure")
|
|
mu["coef_ue"] = coef_ue # coef reel ou coef de l'ue si capitalisee
|
|
|
|
if mu["is_capitalized"]:
|
|
mu["formsemestre_id"] = formsemestre_id
|
|
mu["capitalized_ue_id"] = capitalized_ue_id
|
|
if mu["was_capitalized"]:
|
|
mu["event_date"] = event_date
|
|
# - ECTS ? ("pot" pour "potentiels" car les ECTS ne seront acquises qu'apres validation du jury
|
|
if (
|
|
isinstance(mu["moy"], float)
|
|
and mu["moy"] >= self.parcours.NOTES_BARRE_VALID_UE
|
|
):
|
|
mu["ects_pot"] = ue["ects"] or 0.0
|
|
if ue_is_fondamentale(ue["type"]):
|
|
mu["ects_pot_fond"] = mu["ects_pot"]
|
|
else:
|
|
mu["ects_pot_fond"] = 0.0
|
|
if ue_is_professionnelle(ue["type"]):
|
|
mu["ects_pot_pro"] = mu["ects_pot"]
|
|
else:
|
|
mu["ects_pot_pro"] = 0.0
|
|
else:
|
|
mu["ects_pot"] = 0.0
|
|
mu["ects_pot_fond"] = 0.0
|
|
mu["ects_pot_pro"] = 0.0
|
|
sem_ects_pot += mu["ects_pot"]
|
|
sem_ects_pot_fond += mu["ects_pot_fond"]
|
|
sem_ects_pot_pro += mu["ects_pot_pro"]
|
|
|
|
# - Calcul moyenne générale dans le semestre:
|
|
if mu["is_capitalized"]:
|
|
try:
|
|
sum_notes += mu["moy"] * mu["coef_ue"]
|
|
sum_coefs += mu["coef_ue"]
|
|
except: # pas de note dans cette UE
|
|
pass
|
|
else:
|
|
if mu["coefs_bonus_gen"]:
|
|
notes_bonus_gen.extend(mu["notes_bonus_gen"])
|
|
coefs_bonus_gen.extend(mu["coefs_bonus_gen"])
|
|
#
|
|
try:
|
|
sum_notes += mu["moy"] * mu["sum_coefs"]
|
|
sum_coefs += mu["sum_coefs"]
|
|
nb_notes = nb_notes + 1
|
|
except TypeError:
|
|
nb_missing = nb_missing + 1
|
|
# Le resultat:
|
|
infos = dict(
|
|
nb_notes=nb_notes,
|
|
nb_missing=nb_missing,
|
|
sum_coefs=sum_coefs,
|
|
moy_ues=moy_ues,
|
|
ects_pot=sem_ects_pot,
|
|
ects_pot_fond=sem_ects_pot_fond,
|
|
ects_pot_pro=sem_ects_pot_pro,
|
|
sem=self.sem,
|
|
)
|
|
# ---- Calcul moyenne (avec bonus sport&culture)
|
|
if sum_coefs <= 0 or block_computation:
|
|
infos["moy"] = "NA"
|
|
else:
|
|
if self.use_ue_coefs:
|
|
# Calcul optionnel (mai 2020)
|
|
# moyenne pondére par leurs coefficients des moyennes d'UE
|
|
sum_moy_ue = 0
|
|
sum_coefs_ue = 0
|
|
for mu in moy_ues.values():
|
|
# mu["moy"] can be a number, or "NA", or "ERR" (user-defined UE formulas)
|
|
if (
|
|
(mu["ue"]["type"] != UE_SPORT)
|
|
and scu.isnumber(mu["moy"])
|
|
and (mu["est_inscrit"] or mu["is_capitalized"])
|
|
):
|
|
coef_ue = mu["ue"]["coefficient"]
|
|
sum_moy_ue += mu["moy"] * coef_ue
|
|
sum_coefs_ue += coef_ue
|
|
if sum_coefs_ue != 0:
|
|
infos["moy"] = sum_moy_ue / sum_coefs_ue
|
|
else:
|
|
infos["moy"] = "NA"
|
|
else:
|
|
# Calcul standard ScoDoc: moyenne pondérée des notes de modules
|
|
infos["moy"] = sum_notes / sum_coefs
|
|
|
|
if notes_bonus_gen and infos["moy"] != "NA":
|
|
# regle de calcul maison (configurable, voir bonus_sport.py)
|
|
if sum(coefs_bonus_gen) <= 0 and len(coefs_bonus_gen) != 1:
|
|
log(
|
|
"comp_etud_moy_gen: invalid or null coefficient (%s) for notes_bonus_gen=%s (etudid=%s, formsemestre_id=%s)"
|
|
% (
|
|
coefs_bonus_gen,
|
|
notes_bonus_gen,
|
|
etudid,
|
|
self.formsemestre_id,
|
|
)
|
|
)
|
|
bonus = 0
|
|
else:
|
|
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(
|
|
notes_bonus_gen, coefs_bonus_gen, infos=infos
|
|
)
|
|
else:
|
|
bonus = 0.0
|
|
self.bonus[etudid] = bonus
|
|
infos["moy"] += bonus
|
|
infos["moy"] = min(infos["moy"], 20.0) # clip bogus bonus
|
|
|
|
return infos
|
|
|
|
def get_etud_moy_gen(self, etudid): # -> float | str
|
|
"""Moyenne generale de cet etudiant dans ce semestre.
|
|
Prend en compte les UE capitalisées.
|
|
Si pas de notes: 'NA'
|
|
"""
|
|
return self.moy_gen[etudid]
|
|
|
|
def get_etud_moy_infos(self, etudid): # XXX OBSOLETE
|
|
"""Infos sur moyennes"""
|
|
return self.etud_moy_infos[etudid]
|
|
|
|
# was etud_has_all_ue_over_threshold:
|
|
def etud_check_conditions_ues(self, etudid):
|
|
"""Vrai si les conditions sur les UE sont remplies.
|
|
Ne considère que les UE ayant des notes (moyenne calculée).
|
|
(les UE sans notes ne sont pas comptées comme sous la barre)
|
|
Prend en compte les éventuelles UE capitalisées.
|
|
|
|
Pour les parcours habituels, cela revient à vérifier que
|
|
les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
|
|
|
|
Pour les parcours non standards (LP2014), cela peut être plus compliqué.
|
|
|
|
Return: True|False, message explicatif
|
|
"""
|
|
ue_status_list = []
|
|
for ue in self._ues:
|
|
ue_status = self.get_etud_ue_status(etudid, ue["ue_id"])
|
|
if ue_status:
|
|
ue_status_list.append(ue_status)
|
|
return self.parcours.check_barre_ues(ue_status_list)
|
|
|
|
def get_table_moyennes_triees(self):
|
|
return self.T
|
|
|
|
def get_etud_rang(self, etudid) -> str:
|
|
return self.etud_moy_gen_ranks.get(etudid, "999")
|
|
|
|
def get_etud_rang_group(self, etudid, group_id):
|
|
"""Returns rank of etud in this group and number of etuds in group.
|
|
If etud not in group, returns None.
|
|
"""
|
|
if not group_id in self.rangs_groupes:
|
|
# lazy: fill rangs_groupes on demand
|
|
# { groupe : { etudid : rang } }
|
|
if not group_id in self.group_etuds:
|
|
# lazy fill: list of etud in group_id
|
|
etuds = sco_groups.get_group_members(group_id)
|
|
self.group_etuds[group_id] = set([x["etudid"] for x in etuds])
|
|
# 1- build T restricted to group
|
|
Tr = []
|
|
for t in self.get_table_moyennes_triees():
|
|
t_etudid = t[-1]
|
|
if t_etudid in self.group_etuds[group_id]:
|
|
Tr.append(t)
|
|
#
|
|
self.rangs_groupes[group_id] = comp_ranks(Tr)
|
|
|
|
return (
|
|
self.rangs_groupes[group_id].get(etudid, None),
|
|
len(self.rangs_groupes[group_id]),
|
|
)
|
|
|
|
def get_table_moyennes_dict(self):
|
|
"""{ etudid : (liste des moyennes) } comme get_table_moyennes_triees"""
|
|
D = {}
|
|
for t in self.T:
|
|
D[t[-1]] = t
|
|
return D
|
|
|
|
def get_moduleimpls_attente(self):
|
|
"Liste des moduleimpls avec des notes en attente"
|
|
return self._mods_att
|
|
|
|
# Decisions existantes du jury
|
|
def comp_decisions_jury(self):
|
|
"""Cherche les decisions du jury pour le semestre (pas les UE).
|
|
Calcule l'attribut:
|
|
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
|
|
decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
|
|
Si la decision n'a pas été prise, la clé etudid n'est pas présente.
|
|
Si l'étudiant est défaillant, met un code DEF sur toutes les UE
|
|
"""
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
cursor.execute(
|
|
"""SELECT etudid, code, assidu, compense_formsemestre_id, event_date
|
|
FROM scolar_formsemestre_validation
|
|
WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL;
|
|
""",
|
|
{"formsemestre_id": self.formsemestre_id},
|
|
)
|
|
decisions_jury = {}
|
|
for (
|
|
etudid,
|
|
code,
|
|
assidu,
|
|
compense_formsemestre_id,
|
|
event_date,
|
|
) in cursor.fetchall():
|
|
decisions_jury[etudid] = {
|
|
"code": code,
|
|
"assidu": assidu,
|
|
"compense_formsemestre_id": compense_formsemestre_id,
|
|
"event_date": ndb.DateISOtoDMY(event_date),
|
|
}
|
|
|
|
self.decisions_jury = decisions_jury
|
|
# UEs:
|
|
cursor.execute(
|
|
"select etudid, ue_id, code, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is not NULL;",
|
|
{"formsemestre_id": self.formsemestre_id},
|
|
)
|
|
decisions_jury_ues = {}
|
|
for (etudid, ue_id, code, event_date) in cursor.fetchall():
|
|
if etudid not in decisions_jury_ues:
|
|
decisions_jury_ues[etudid] = {}
|
|
# Calcul des ECTS associes a cette UE:
|
|
ects = 0.0
|
|
if sco_codes_parcours.code_ue_validant(code):
|
|
ue = self.uedict.get(ue_id, None)
|
|
if ue is None: # not in list for this sem ??? (probably an error)
|
|
log(
|
|
"Warning: %s capitalized an UE %s which is not part of current sem %s"
|
|
% (etudid, ue_id, self.formsemestre_id)
|
|
)
|
|
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
|
|
self.uedict[ue_id] = ue # record this UE
|
|
if ue_id not in self._uecoef:
|
|
cl = formsemestre_uecoef_list(
|
|
cnx,
|
|
args={
|
|
"formsemestre_id": self.formsemestre_id,
|
|
"ue_id": ue_id,
|
|
},
|
|
)
|
|
if not cl:
|
|
# cas anormal: UE capitalisee, pas dans ce semestre, et sans coef
|
|
log("Warning: setting UE coef to zero")
|
|
formsemestre_uecoef_create(
|
|
cnx,
|
|
args={
|
|
"formsemestre_id": self.formsemestre_id,
|
|
"ue_id": ue_id,
|
|
"coefficient": 0,
|
|
},
|
|
)
|
|
|
|
ects = ue["ects"] or 0.0 # 0 if None
|
|
|
|
decisions_jury_ues[etudid][ue_id] = {
|
|
"code": code,
|
|
"ects": ects, # 0. si non UE validée ou si mode de calcul different (?)
|
|
"event_date": ndb.DateISOtoDMY(event_date),
|
|
}
|
|
|
|
self.decisions_jury_ues = decisions_jury_ues
|
|
|
|
def get_etud_decision_sem(self, etudid):
|
|
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
|
|
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
|
|
Si état défaillant, force le code a DEF
|
|
"""
|
|
if self.get_etud_etat(etudid) == DEF:
|
|
return {
|
|
"code": DEF,
|
|
"assidu": False,
|
|
"event_date": "",
|
|
"compense_formsemestre_id": None,
|
|
}
|
|
else:
|
|
return self.decisions_jury.get(etudid, None)
|
|
|
|
def get_etud_decision_ues(self, etudid):
|
|
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
|
|
Ne tient pas compte des UE capitalisées.
|
|
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
|
|
Ne renvoie aucune decision d'UE pour les défaillants
|
|
"""
|
|
if self.get_etud_etat(etudid) == DEF:
|
|
return {}
|
|
else:
|
|
return self.decisions_jury_ues.get(etudid, None)
|
|
|
|
def sem_has_decisions(self):
|
|
"""True si au moins une decision de jury dans ce semestre"""
|
|
if [x for x in self.decisions_jury_ues.values() if x]:
|
|
return True
|
|
|
|
return len([x for x in self.decisions_jury_ues.values() if x]) > 0
|
|
|
|
def etud_has_decision(self, etudid):
|
|
"""True s'il y a une décision de jury pour cet étudiant"""
|
|
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
|
|
|
|
def all_etuds_have_sem_decisions(self):
|
|
"""True si tous les étudiants du semestre ont une décision de jury.
|
|
ne regarde pas les décisions d'UE (todo: à voir ?)
|
|
"""
|
|
for etudid in self.get_etudids():
|
|
if self.inscrdict[etudid]["etat"] == "D":
|
|
continue # skip demissionnaires
|
|
if self.get_etud_decision_sem(etudid) is None:
|
|
return False
|
|
return True
|
|
|
|
# Capitalisation des UEs
|
|
def comp_ue_capitalisees(self):
|
|
"""Cherche pour chaque etudiant ses UE capitalisées dans ce semestre.
|
|
Calcule l'attribut:
|
|
ue_capitalisees = { etudid :
|
|
[{ 'moy':, 'event_date' : ,'formsemestre_id' : }, ...] }
|
|
"""
|
|
self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
|
|
cnx = None
|
|
semestre_id = self.sem["semestre_id"]
|
|
for etudid in self.get_etudids():
|
|
capital = formsemestre_get_etud_capitalisation(
|
|
self.formation["id"],
|
|
semestre_id,
|
|
ndb.DateDMYtoISO(self.sem["date_debut"]),
|
|
etudid,
|
|
)
|
|
for ue_cap in capital:
|
|
# Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
|
|
# il faut la calculer ici et l'enregistrer
|
|
if ue_cap["moy_ue"] is None:
|
|
log(
|
|
"comp_ue_capitalisees: recomputing UE moy (etudid=%s, ue_id=%s formsemestre_id=%s)"
|
|
% (etudid, ue_cap["ue_id"], ue_cap["formsemestre_id"])
|
|
)
|
|
nt_cap = sco_cache.NotesTableCache.get(
|
|
ue_cap["formsemestre_id"]
|
|
) # > UE capitalisees par un etud
|
|
ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])
|
|
if ue_cap_status:
|
|
moy_ue_cap = ue_cap_status["moy"]
|
|
else:
|
|
moy_ue_cap = ""
|
|
ue_cap["moy_ue"] = moy_ue_cap
|
|
if (
|
|
isinstance(moy_ue_cap, float)
|
|
and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE
|
|
):
|
|
if not cnx:
|
|
cnx = ndb.GetDBConnexion()
|
|
sco_cursus_dut.do_formsemestre_validate_ue(
|
|
cnx,
|
|
nt_cap,
|
|
ue_cap["formsemestre_id"],
|
|
etudid,
|
|
ue_cap["ue_id"],
|
|
ue_cap["code"],
|
|
)
|
|
else:
|
|
log(
|
|
"*** valid inconsistency: moy_ue_cap=%s (etudid=%s, ue_id=%s formsemestre_id=%s)"
|
|
% (
|
|
moy_ue_cap,
|
|
etudid,
|
|
ue_cap["ue_id"],
|
|
ue_cap["formsemestre_id"],
|
|
)
|
|
)
|
|
ue_cap["moy"] = ue_cap["moy_ue"] # backward compat (needs refactoring)
|
|
self.ue_capitalisees[etudid].append(ue_cap)
|
|
if cnx:
|
|
cnx.commit()
|
|
# log('comp_ue_capitalisees=\n%s' % pprint.pformat(self.ue_capitalisees) )
|
|
|
|
# def comp_etud_sum_coef_modules_ue( etudid, ue_id):
|
|
# """Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
|
|
# ou None s'il n'y a aucun module
|
|
# """
|
|
# c_list = [ mod['module']['coefficient']
|
|
# for mod in self._modimpls
|
|
# if (( mod['module']['ue_id'] == ue_id)
|
|
# and self._modmoys[mod['moduleimpl_id']].get(etudid, False) is not False)
|
|
# ]
|
|
# if not c_list:
|
|
# return None
|
|
# return sum(c_list)
|
|
|
|
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap, cnx=None):
|
|
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
|
|
injectée dans le semestre courant.
|
|
|
|
ue : ue du semestre courant
|
|
|
|
ue_cap = resultat de formsemestre_get_etud_capitalisation
|
|
{ 'ue_id' (dans le semestre source),
|
|
'ue_code', 'moy', 'event_date','formsemestre_id' }
|
|
"""
|
|
# log("get_etud_ue_cap_coef\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s\n" % (self.formsemestre_id, etudid, ue, ue_cap))
|
|
# 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
|
|
if ue["ue_id"] not in self._uecoef:
|
|
self._uecoef[ue["ue_id"]] = formsemestre_uecoef_list(
|
|
cnx,
|
|
args={"formsemestre_id": self.formsemestre_id, "ue_id": ue["ue_id"]},
|
|
)
|
|
|
|
if len(self._uecoef[ue["ue_id"]]):
|
|
# utilisation du coef manuel
|
|
return self._uecoef[ue["ue_id"]][0]["coefficient"]
|
|
|
|
# 2- Mode automatique: calcul du coefficient
|
|
# Capitalisation depuis un autre semestre ScoDoc ?
|
|
coef = None
|
|
if ue_cap["formsemestre_id"]:
|
|
# Somme des coefs dans l'UE du semestre d'origine (nouveau: 23/01/2016)
|
|
coef = comp_etud_sum_coef_modules_ue(
|
|
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
|
|
)
|
|
if coef != None:
|
|
return coef
|
|
else:
|
|
# Capitalisation UE externe: quel coef appliquer ?
|
|
# Si l'étudiant est inscrit dans le semestre courant,
|
|
# somme des coefs des modules de l'UE auxquels il est inscrit
|
|
c = comp_etud_sum_coef_modules_ue(self.formsemestre_id, etudid, ue["ue_id"])
|
|
if c is not None: # inscrit à au moins un module de cette UE
|
|
return c
|
|
# arfff: aucun moyen de déterminer le coefficient de façon sûre
|
|
log(
|
|
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s\nue_cap=%s"
|
|
% (self.formsemestre_id, etudid, ue, ue_cap)
|
|
)
|
|
raise ScoValueError(
|
|
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
|
|
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
|
|
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
|
|
</div>
|
|
"""
|
|
% (
|
|
ue["acronyme"],
|
|
url_for(
|
|
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
|
),
|
|
self.get_nom_long(etudid),
|
|
url_for(
|
|
"notes.formsemestre_edit_uecoefs",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=self.formsemestre_id,
|
|
err_ue_id=ue["ue_id"],
|
|
),
|
|
)
|
|
)
|
|
|
|
return 0.0 # ?
|
|
|
|
def get_etud_ue_status(self, etudid, ue_id):
|
|
"Etat de cette UE (note, coef, capitalisation, ...)"
|
|
return self._etud_moy_ues[etudid][ue_id]
|
|
|
|
def etud_has_notes_attente(self, etudid):
|
|
"""Vrai si cet etudiant a au moins une note en attente dans ce semestre.
|
|
(ne compte que les notes en attente dans des évaluation avec coef. non nul).
|
|
"""
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
cursor.execute(
|
|
"""SELECT n.*
|
|
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
|
|
notes_moduleimpl_inscription i
|
|
WHERE n.etudid = %(etudid)s
|
|
and n.value = %(code_attente)s
|
|
and n.evaluation_id = e.id
|
|
and e.moduleimpl_id = m.id
|
|
and m.formsemestre_id = %(formsemestre_id)s
|
|
and e.coefficient != 0
|
|
and m.id = i.moduleimpl_id
|
|
and i.etudid=%(etudid)s
|
|
""",
|
|
{
|
|
"formsemestre_id": self.formsemestre_id,
|
|
"etudid": etudid,
|
|
"code_attente": scu.NOTES_ATTENTE,
|
|
},
|
|
)
|
|
return len(cursor.fetchall()) > 0
|
|
|
|
def get_evaluations_etats(self): # evaluation_list_in_sem
|
|
"""[ {...evaluation et son etat...} ]"""
|
|
if self._evaluations_etats is None:
|
|
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
|
|
self.formsemestre_id
|
|
)
|
|
|
|
return self._evaluations_etats
|
|
|
|
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
|
|
"""Liste des évaluations de ce module"""
|
|
return [
|
|
e
|
|
for e in self.get_evaluations_etats()
|
|
if e["moduleimpl_id"] == moduleimpl_id
|
|
]
|
|
|
|
def apc_recompute_moyennes(self):
|
|
"""recalcule les moyennes en APC (BUT)
|
|
et modifie en place le tableau T.
|
|
XXX Raccord provisoire avant refonte de cette classe.
|
|
"""
|
|
assert self.parcours.APC_SAE
|
|
formsemestre = FormSemestre.query.get(self.formsemestre_id)
|
|
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
|
|
|
# Rappel des épisodes précédents: T est une liste de liste
|
|
# Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
|
|
ues = self.get_ues_stat_dict() # incluant le(s) UE de sport
|
|
for t in self.T:
|
|
etudid = t[-1]
|
|
if etudid in results.etud_moy_gen: # evite les démissionnaires
|
|
t[0] = results.etud_moy_gen[etudid]
|
|
for i, ue in enumerate(ues, start=1):
|
|
if ue["type"] != UE_SPORT:
|
|
# temporaire pour 9.1.29 !
|
|
if ue["id"] in results.etud_moy_ue:
|
|
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
|
else:
|
|
t[i] = ""
|
|
# re-trie selon la nouvelle moyenne générale:
|
|
self.T.sort(key=self._row_key)
|
|
# Remplace aussi le rang:
|
|
self.etud_moy_gen_ranks = results.etud_moy_gen_ranks
|