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

This commit is contained in:
leonard_montalbano 2022-03-01 12:58:47 +01:00
commit 1c271bbad4
15 changed files with 189 additions and 107 deletions

View File

@ -198,7 +198,10 @@ class BonusSportAdditif(BonusSport):
à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte
seuil_comptage = (
None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen)
)
proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@ -211,10 +214,13 @@ class BonusSportAdditif(BonusSport):
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
seuil_comptage = (
self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
)
bonus_moy_arr = np.sum(
np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
(sem_modimpl_moys_inscrits - self.seuil_comptage)
* self.proportion_point,
0.0,
),
@ -338,9 +344,12 @@ class BonusAisneStQuentin(BonusSportAdditif):
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
@ -604,8 +613,9 @@ class BonusLaRochelle(BonusSportAdditif):
name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.01
seuil_moy_gen = 10.0 # si bonus > 10,
seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1%
class BonusLeHavre(BonusSportMultiplicatif):
@ -823,9 +833,11 @@ class BonusVilleAvray(BonusSport):
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2

View File

@ -40,13 +40,11 @@ def compute_mat_moys_classic(
modimpl_mask = np.array(
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
)
etud_moy_gen, _, _ = moy_ue.compute_ue_moys_classic(
formsemestre,
etud_moy_mat = moy_ue.compute_mat_moys_classic(
sem_matrix=sem_matrix,
ues=ues,
modimpl_inscr_df=modimpl_inscr_df,
modimpl_coefs=modimpl_coefs,
modimpl_mask=modimpl_mask,
)
matiere_moy[matiere_id] = etud_moy_gen
matiere_moy[matiere_id] = etud_moy_mat
return matiere_moy

View File

@ -294,7 +294,8 @@ def compute_ue_moys_classic(
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique.
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
@ -363,7 +364,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues
)
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
@ -408,6 +409,68 @@ def compute_ue_moys_classic(
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
def compute_mat_moys_classic(
sem_matrix: np.array,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> pd.Series:
"""Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
La moyenne est un nombre (note/20 ou NaN.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
modimpl_mask: masque des modimpls à prendre en compte
Résultat:
- moyennes: pd.Series, index etudid
"""
if (not len(modimpl_mask)) or (
sem_matrix.shape[0] == 0
): # aucun module ou aucun étudiant
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
return pd.Series(
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
)
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
# Enlève les NaN du numérateur:
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN (nb_etuds x nb_mods)
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
axis=1
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,

View File

@ -8,11 +8,13 @@
"""
from flask import g
from app import db
from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it.
"""
is_apc = formsemestre.formation.is_apc()
if is_apc and formsemestre.semestre_id == -1:
formsemestre.semestre_id = 1
db.session.add(formsemestre)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre.id)
# --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_results_cache = {}
@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id]
klass = (
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id]

View File

@ -36,6 +36,7 @@
"""
from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre
@ -97,8 +98,12 @@ def pe_view_sem_recap(
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
template_latex = avis_tmpl_file.read().decode('utf-8')
template_latex = template_latex
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
@ -114,7 +119,7 @@ def pe_view_sem_recap(
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode('utf-8')
footer_latex = footer_tmpl_file.read().decode("utf-8")
footer_latex = footer_latex
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(

View File

@ -323,9 +323,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
if ue_status["coef_ue"] != None:
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else:
# C'est un bug:
log("u=" + pprint.pformat(u))
raise Exception("invalid None coef for ue")
u["coef_ue_txt"] = "-"
if (
dpv

View File

@ -33,17 +33,12 @@
"""
# API ScoDoc8 pour les caches:
# sco_cache.NotesTableCache.get( formsemestre_id)
# => sco_cache.NotesTableCache.get(formsemestre_id)
# API pour les caches:
# sco_cache.MyCache.get( formsemestre_id)
# => sco_cache.MyCache.get(formsemestre_id)
#
# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None)
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False)
#
#
# Nouvelles fonctions:
# sco_cache.NotesTableCache.delete(formsemestre_id)
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
# sco_cache.MyCache.delete(formsemestre_id)
# sco_cache.MyCache.delete_many(formsemestre_id_list)
#
# Bulletins PDF:
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h
class NotesTableCache(ScoDocCache):
"""Cache pour les NotesTable
Clé: formsemestre_id
Valeur: NotesTable instance
"""
prefix = "NT"
@classmethod
def get(cls, formsemestre_id, compute=True):
"""Returns NotesTable for this formsemestre
Search in local cache (g.nt_cache) or global app cache (eg REDIS)
If not in cache:
If compute is True, build it and cache it
Else return None
"""
# try local cache (same request)
if not hasattr(g, "nt_cache"):
g.nt_cache = {}
else:
if formsemestre_id in g.nt_cache:
return g.nt_cache[formsemestre_id]
# try REDIS
key = cls._get_key(formsemestre_id)
nt = CACHE.get(key)
if nt:
g.nt_cache[formsemestre_id] = nt # cache locally (same request)
return nt
if not compute:
return None
# Recompute requested table:
from app.scodoc import notes_table
t0 = time.time()
nt = notes_table.NotesTable(formsemestre_id)
t1 = time.time()
_ = cls.set(formsemestre_id, nt) # cache in REDIS
t2 = time.time()
log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)")
g.nt_cache[formsemestre_id] = nt
return nt
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False
):
@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if not pdfonly:
# Delete cached notes and evaluations
NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id:
for fid in formsemestre_ids:
EvaluationCache.invalidate_sem(fid)
if hasattr(g, "nt_cache") and fid in g.nt_cache:
del g.nt_cache[fid]
if (
hasattr(g, "formsemestre_results_cache")
and fid in g.formsemestre_results_cache
):
del g.formsemestre_results_cache[fid]
else:
# optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems()
if hasattr(g, "nt_cache"):
del g.nt_cache
if hasattr(g, "formsemestre_results_cache"):
del g.formsemestre_results_cache
SemInscriptionsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
class DefferedSemCacheManager:

View File

@ -51,6 +51,7 @@ import fcntl
import subprocess
import requests
from flask import flash
from flask_login import current_user
import app.scodoc.notesdb as ndb
@ -124,6 +125,7 @@ def sco_dump_and_send_db():
fcntl.flock(x, fcntl.LOCK_UN)
log("sco_dump_and_send_db: done.")
flash("Données envoyées au serveur d'assistance")
return "\n".join(H) + html_sco_header.sco_footer()

View File

@ -151,8 +151,14 @@ def formation_export(
if mod["ects"] is None:
del mod["ects"]
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult(
F, name="formation", format=format, force_outer_xml_tag=False, attached=True
F,
name="formation",
format=format,
force_outer_xml_tag=False,
attached=True,
filename=filename,
)

View File

@ -78,7 +78,7 @@ def formsemestre_createwithmodules():
H = [
html_sco_header.sco_header(
page_title="Création d'un semestre",
javascripts=["libjs/AutoSuggest.js"],
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
),
@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
javascripts=["libjs/AutoSuggest.js"],
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
# en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
semestre_id_list = list(range(1, NB_SEM + 1))
if not formation.is_apc():
# propose "pas de semestre" seulement en classique
semestre_id_list.insert(0, -1)
semestre_id_labels = []
for sid in semestre_id_list:
@ -341,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc()
else "",
"attributes": ['onchange="change_semestre_id();"']
if formation.is_apc()
else "",
},
),
)
@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
{
"input_type": "boolcheckbox",
"title": "",
"explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations",
"explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""",
},
),
(
@ -534,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False):
]
nbmod = 0
if edit:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"
else:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td></tr>"
for semestre_id in semestre_ids:
if formation.is_apc():
# pour restreindre l'édition aux module du semestre sélectionné
tr_class = f'class="sem{semestre_id}"'
else:
tr_class = ""
if edit:
templ_sep = f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"""
else:
templ_sep = (
f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td></tr>"""
)
modform.append(
(
"sep",
@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
)
fcg += "</select>"
itemtemplate = (
"""<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
+ fcg
+ "</td></tr>"
)
else:
itemtemplate = """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
modform.append(
(
"MI" + str(mod["module_id"]),

View File

@ -987,7 +987,6 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML"""
# porté du DTML
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id

View File

@ -645,21 +645,30 @@ class ScoDocJSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
def sendJSON(data, attached=False):
def sendJSON(data, attached=False, filename=None):
js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
return send_file(
js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached
js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached
)
def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True):
def sendXML(
data,
tagname=None,
force_outer_xml_tag=True,
attached=False,
quote=True,
filename=None,
):
if type(data) != list:
data = [data] # always list-of-dicts
if force_outer_xml_tag:
data = [{tagname: data}]
tagname += "_list"
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote)
return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached)
return send_file(
doc, filename=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached
)
def sendResult(
@ -669,6 +678,7 @@ def sendResult(
force_outer_xml_tag=True,
attached=False,
quote_xml=True,
filename=None,
):
if (format is None) or (format == "html"):
return data
@ -679,9 +689,10 @@ def sendResult(
force_outer_xml_tag=force_outer_xml_tag,
attached=attached,
quote=quote_xml,
filename=filename,
)
elif format == "json":
return sendJSON(data, attached=attached)
return sendJSON(data, attached=attached, filename=filename)
else:
raise ValueError("invalid format: %s" % format)

View File

@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits {
text-align: center;
}
td.formsemestre_status_code {
width: 2em;
/* width: 2em; */
padding-right: 1em;
}

View File

@ -0,0 +1,14 @@
// Formulaire formsemestre_createwithmodules
function change_semestre_id() {
var semestre_id = $("#tf_semestre_id")[0].value;
for (var i = -1; i < 12; i++) {
$(".sem" + i).hide();
}
$(".sem" + semestre_id).show();
}
$(window).on('load', function () {
change_semestre_id();
});

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.1.66"
SCOVERSION = "9.1.67"
SCONAME = "ScoDoc"