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

This commit is contained in:
Arthur ZHU 2022-03-03 09:47:42 +01:00
commit 60552feec7
24 changed files with 329 additions and 130 deletions

View File

@ -198,7 +198,10 @@ class BonusSportAdditif(BonusSport):
à la moyenne générale du semestre déjà obtenue par l'étudiant. à 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 proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@ -211,11 +214,13 @@ class BonusSportAdditif(BonusSport):
if 0 in sem_modimpl_moys_inscrits.shape: if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
return return
seuil_comptage = (
self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
)
bonus_moy_arr = np.sum( bonus_moy_arr = np.sum(
np.where( np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen, sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen) (sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
* self.proportion_point,
0.0, 0.0,
), ),
axis=1, axis=1,
@ -338,9 +343,12 @@ class BonusAisneStQuentin(BonusSportAdditif):
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
return return
# Calcule moyenne pondérée des notes de sport: # Calcule moyenne pondérée des notes de sport:
bonus_moy_arr = np.sum( with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 bonus_moy_arr = np.sum(
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) 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 < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5 bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4 bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
@ -604,8 +612,9 @@ class BonusLaRochelle(BonusSportAdditif):
name = "bonus_iutlr" name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle" displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # tous les points sont comptés seuil_moy_gen = 10.0 # si bonus > 10,
proportion_point = 0.01 seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1%
class BonusLeHavre(BonusSportMultiplicatif): class BonusLeHavre(BonusSportMultiplicatif):
@ -823,9 +832,11 @@ class BonusVilleAvray(BonusSport):
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
return return
# Calcule moyenne pondérée des notes de sport: # Calcule moyenne pondérée des notes de sport:
bonus_moy_arr = np.sum( with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 bonus_moy_arr = np.sum(
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) 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 < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3 bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 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( modimpl_mask = np.array(
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted] [m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
) )
etud_moy_gen, _, _ = moy_ue.compute_ue_moys_classic( etud_moy_mat = moy_ue.compute_mat_moys_classic(
formsemestre,
sem_matrix=sem_matrix, sem_matrix=sem_matrix,
ues=ues,
modimpl_inscr_df=modimpl_inscr_df, modimpl_inscr_df=modimpl_inscr_df,
modimpl_coefs=modimpl_coefs, modimpl_coefs=modimpl_coefs,
modimpl_mask=modimpl_mask, modimpl_mask=modimpl_mask,
) )
matiere_moy[matiere_id] = etud_moy_gen matiere_moy[matiere_id] = etud_moy_mat
return matiere_moy return matiere_moy

View File

@ -294,7 +294,8 @@ def compute_ue_moys_classic(
modimpl_coefs: np.array, modimpl_coefs: np.array,
modimpl_mask: np.array, modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]: ) -> 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 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 NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles 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_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues [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) coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float) 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 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( def compute_malus(
formsemestre: FormSemestre, formsemestre: FormSemestre,
sem_modimpl_moys: np.array, sem_modimpl_moys: np.array,

View File

@ -8,11 +8,13 @@
""" """
from flask import g from flask import g
from app import db
from app.comp.jury import ValidationsSemestre from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: 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) Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it. 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) # --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"): if not hasattr(g, "formsemestre_results_cache"):
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: if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id] return g.formsemestre_results_cache[formsemestre.id]
klass = ( klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre) g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id] return g.formsemestre_results_cache[formsemestre.id]

View File

@ -207,7 +207,11 @@ class FormSemestre(db.Model):
modimpls = self.modimpls.all() modimpls = self.modimpls.all()
if self.formation.is_apc(): if self.formation.is_apc():
modimpls.sort( modimpls.sort(
key=lambda m: (m.module.module_type, m.module.numero, m.module.code) key=lambda m: (
m.module.module_type or 0,
m.module.numero or 0,
m.module.code or 0,
)
) )
else: else:
modimpls.sort( modimpls.sort(

View File

@ -36,6 +36,7 @@
""" """
from flask import send_file, request from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -97,8 +98,12 @@ def pe_view_sem_recap(
template_latex = "" template_latex = ""
# template fourni via le formulaire Web # template fourni via le formulaire Web
if avis_tmpl_file: if avis_tmpl_file:
template_latex = avis_tmpl_file.read().decode('utf-8') try:
template_latex = template_latex 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: else:
# template indiqué dans préférences ScoDoc ? # template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference( template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
@ -114,7 +119,7 @@ def pe_view_sem_recap(
footer_latex = "" footer_latex = ""
# template fourni via le formulaire Web # template fourni via le formulaire Web
if footer_tmpl_file: 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 footer_latex = footer_latex
else: else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(

View File

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

View File

@ -33,17 +33,12 @@
""" """
# API ScoDoc8 pour les caches: # API pour les caches:
# sco_cache.NotesTableCache.get( formsemestre_id) # sco_cache.MyCache.get( formsemestre_id)
# => sco_cache.NotesTableCache.get(formsemestre_id) # => sco_cache.MyCache.get(formsemestre_id)
# #
# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None) # sco_cache.MyCache.delete(formsemestre_id)
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False) # sco_cache.MyCache.delete_many(formsemestre_id_list)
#
#
# Nouvelles fonctions:
# sco_cache.NotesTableCache.delete(formsemestre_id)
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
# #
# Bulletins PDF: # Bulletins PDF:
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version) # sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h 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) def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
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: if not pdfonly:
# Delete cached notes and evaluations # Delete cached notes and evaluations
NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id: if formsemestre_id:
for fid in formsemestre_ids: for fid in formsemestre_ids:
EvaluationCache.invalidate_sem(fid) EvaluationCache.invalidate_sem(fid)
if hasattr(g, "nt_cache") and fid in g.nt_cache: if (
del g.nt_cache[fid] hasattr(g, "formsemestre_results_cache")
and fid in g.formsemestre_results_cache
):
del g.formsemestre_results_cache[fid]
else: else:
# optimization when we invalidate all evaluations: # optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems() EvaluationCache.invalidate_all_sems()
if hasattr(g, "nt_cache"): if hasattr(g, "formsemestre_results_cache"):
del g.nt_cache del g.formsemestre_results_cache
SemInscriptionsCache.delete_many(formsemestre_ids) SemInscriptionsCache.delete_many(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager: class DefferedSemCacheManager:

View File

@ -51,6 +51,7 @@ import fcntl
import subprocess import subprocess
import requests import requests
from flask import flash
from flask_login import current_user from flask_login import current_user
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -124,6 +125,7 @@ def sco_dump_and_send_db():
fcntl.flock(x, fcntl.LOCK_UN) fcntl.flock(x, fcntl.LOCK_UN)
log("sco_dump_and_send_db: done.") 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() return "\n".join(H) + html_sco_header.sco_footer()

View File

@ -52,6 +52,7 @@ def html_edit_formation_apc(
""" """
parcours = formation.get_parcours() parcours = formation.get_parcours()
assert parcours.APC_SAE assert parcours.APC_SAE
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by( ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
Module.semestre_id, Module.numero, Module.code Module.semestre_id, Module.numero, Module.code
) )
@ -68,6 +69,19 @@ def html_edit_formation_apc(
).order_by( ).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
) )
ues_by_sem = {}
ects_by_sem = {}
for semestre_idx in semestre_ids:
ues_by_sem[semestre_idx] = formation.ues.filter_by(
semestre_idx=semestre_idx
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
ects = [ue.ects for ue in ues_by_sem[semestre_idx]]
if None in ects:
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
else:
ects_by_sem[semestre_idx] = sum(ects)
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
icons = { icons = {
@ -93,7 +107,8 @@ def html_edit_formation_apc(
editable=editable, editable=editable,
tag_editable=tag_editable, tag_editable=tag_editable,
icons=icons, icons=icons,
UniteEns=UniteEns, ues_by_sem=ues_by_sem,
ects_by_sem=ects_by_sem,
), ),
] ]
for semestre_idx in semestre_ids: for semestre_idx in semestre_ids:

View File

@ -562,7 +562,7 @@ def module_edit(module_id=None):
"code", "code",
{ {
"size": 10, "size": 10,
"explanation": "code du module (doit être unique dans la formation)", "explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)",
"allow_null": False, "allow_null": False,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id, module_id=module_id val, field, formation_id, module_id=module_id
@ -701,7 +701,10 @@ def module_edit(module_id=None):
{ {
"title": "Code Apogée", "title": "Code Apogée",
"size": 25, "size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", "explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP
séparés par des virgules (ce code est propre à chaque établissement, se rapprocher
du référent Apogée).
""",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN, "validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
}, },
), ),

View File

@ -305,7 +305,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
( (
"numero", "numero",
{ {
"size": 2, "size": 4,
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage", "explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
"type": "int", "type": "int",
}, },
@ -722,16 +722,16 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<a href="{url_for('notes.refcomp_show', <a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"> scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long} {formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
</a> """ </a>&nbsp;"""
msg_refcomp = "changer" msg_refcomp = "changer"
H.append( H.append(
f""" f"""
<ul> <ul>
<li>{descr_refcomp}&nbsp; <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation', <li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation_id) scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}">{msg_refcomp}</a> }">{msg_refcomp}</a>
</li> </li>
<li><a class="stdlink" href="{ <li> <a class="stdlink" href="{
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx) url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
}">éditer les coefficients des ressources et SAÉs</a> }">éditer les coefficients des ressources et SAÉs</a>
</li> </li>

View File

@ -151,8 +151,14 @@ def formation_export(
if mod["ects"] is None: if mod["ects"] is None:
del mod["ects"] del mod["ects"]
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult( 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 = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Création d'un semestre", page_title="Création d'un semestre",
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"], cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')", bodyOnLoad="init_tf_form('')",
), ),
@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Modification du semestre", "Modification du semestre",
javascripts=["libjs/AutoSuggest.js"], javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"], cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')", bodyOnLoad="init_tf_form('')",
) )
@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
# en APC, ne permet pas de changer de semestre # en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id] semestre_id_list = [formsemestre.semestre_id]
else: 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 = [] semestre_id_labels = []
for sid in semestre_id_list: 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" "explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc() if formation.is_apc()
else "", else "",
"attributes": ['onchange="change_semestre_id();"']
if formation.is_apc()
else "",
}, },
), ),
) )
@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
{ {
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"title": "", "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 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: 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( modform.append(
( (
"sep", "sep",
@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
) )
fcg += "</select>" fcg += "</select>"
itemtemplate = ( 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 + fcg
+ "</td></tr>" + "</td></tr>"
) )
else: 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( modform.append(
( (
"MI" + str(mod["module_id"]), "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): def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML""" """Tableau de bord semestre HTML"""
# porté du DTML # porté du DTML
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id

View File

@ -0,0 +1,96 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Exports groupes
"""
from flask import request
from app.scodoc import notesdb as ndb
from app.scodoc import sco_excel
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
import app.scodoc.sco_utils as scu
import sco_version
def groups_list_annotation(group_ids: list[int]) -> list[dict]:
"""Renvoie la liste des annotations pour les groupes d"étudiants indiqués
Arg: liste des id de groupes
Clés: etudid, ine, nip, nom, prenom, date, comment
"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
annotations = []
for group_id in group_ids:
cursor.execute(
"""SELECT i.id AS etudid, i.code_nip, i.code_ine, i.nom, i.prenom, ea.date, ea.comment
FROM group_membership gm, identite i, etud_annotations ea
WHERE gm.group_id=%(group_ids)s
AND gm.etudid=i.id
AND i.id=ea.etudid
""",
{"group_ids": group_id},
)
annotations += cursor.dictfetchall()
return annotations
def groups_export_annotations(group_ids, formsemestre_id=None, format="html"):
"""Les annotations"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id
)
annotations = groups_list_annotation(groups_infos.group_ids)
for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
if format == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else:
columns_ids = ("etudid", "nom", "prenom", "date_str", "comment")
table = GenTable(
rows=annotations,
columns_ids=columns_ids,
titles={
"etudid": "etudid",
"nom": "Nom",
"prenom": "Prénom",
"date": "Date",
"date_str": "Date",
"comment": "Annotation",
},
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
page_title=f"Annotations sur les étudiants de {groups_infos.groups_titles}",
caption="Annotations",
base_url=groups_infos.base_url,
html_sortable=True,
html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
return table.make_page(format=format)

View File

@ -826,6 +826,8 @@ def tab_absences_html(groups_infos, etat=None):
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="trombino?%s&format=pdflist">Liste d'appel avec photos</a></li>""" """<li><a class="stdlink" href="trombino?%s&format=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
% groups_infos.groups_query_args,
"</ul>", "</ul>",
] ]
) )

View File

@ -645,21 +645,30 @@ class ScoDocJSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o) 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) js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
return send_file( 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: if type(data) != list:
data = [data] # always list-of-dicts data = [data] # always list-of-dicts
if force_outer_xml_tag: if force_outer_xml_tag:
data = [{tagname: data}] data = [{tagname: data}]
tagname += "_list" tagname += "_list"
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote) 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( def sendResult(
@ -669,6 +678,7 @@ def sendResult(
force_outer_xml_tag=True, force_outer_xml_tag=True,
attached=False, attached=False,
quote_xml=True, quote_xml=True,
filename=None,
): ):
if (format is None) or (format == "html"): if (format is None) or (format == "html"):
return data return data
@ -679,9 +689,10 @@ def sendResult(
force_outer_xml_tag=force_outer_xml_tag, force_outer_xml_tag=force_outer_xml_tag,
attached=attached, attached=attached,
quote=quote_xml, quote=quote_xml,
filename=filename,
) )
elif format == "json": elif format == "json":
return sendJSON(data, attached=attached) return sendJSON(data, attached=attached, filename=filename)
else: else:
raise ValueError("invalid format: %s" % format) raise ValueError("invalid format: %s" % format)
@ -799,7 +810,7 @@ def abbrev_prenom(prenom):
# #
def timedate_human_repr(): def timedate_human_repr():
"representation du temps courant pour utilisateur: a localiser" "representation du temps courant pour utilisateur"
return time.strftime("%d/%m/%Y à %Hh%M") return time.strftime("%d/%m/%Y à %Hh%M")

View File

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

@ -3,11 +3,9 @@
<div class="formation_list_ues"> <div class="formation_list_ues">
<div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div> <div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div>
{% for semestre_idx in semestre_ids %} {% for semestre_idx in semestre_ids %}
<div class="formation_list_ues_sem">Semestre S{{semestre_idx}}</div> <div class="formation_list_ues_sem">Semestre S{{semestre_idx}} (ECTS: {{ects_by_sem[semestre_idx] | safe}})</div>
<ul class="apc_ue_list"> <ul class="apc_ue_list">
{% for ue in formation.ues.filter_by(semestre_idx=semestre_idx).order_by( {% for ue in ues_by_sem[semestre_idx] %}
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
) %}
<li class="notes_ue_list"> <li class="notes_ue_list">
{% if editable and not loop.first %} {% if editable and not loop.first %}
<a href="{{ url_for('notes.ue_move', <a href="{{ url_for('notes.ue_move',

View File

@ -611,8 +611,7 @@ def SignaleAbsenceGrSemestre(
"""<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n""" """<option value="%(modimpl_id)s" %(sel)s>%(modname)s</option>\n"""
% { % {
"modimpl_id": modimpl["moduleimpl_id"], "modimpl_id": modimpl["moduleimpl_id"],
"modname": modimpl["module"]["code"] "modname": (modimpl["module"]["code"] or "")
or ""
+ " " + " "
+ (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]), + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]),
"sel": sel, "sel": sel,
@ -624,7 +623,7 @@ def SignaleAbsenceGrSemestre(
sel = "selected" # aucun module specifie sel = "selected" # aucun module specifie
H.append( H.append(
"""<p> """<p>
Module concerné par ces absences (%(optionel_txt)s): Module concerné par ces absences (%(optionel_txt)s):
<select id="moduleimpl_id" name="moduleimpl_id" <select id="moduleimpl_id" name="moduleimpl_id"
onchange="document.location='%(url)s&moduleimpl_id='+document.getElementById('moduleimpl_id').value"> onchange="document.location='%(url)s&moduleimpl_id='+document.getElementById('moduleimpl_id').value">
<option value="" %(sel)s>non spécifié</option> <option value="" %(sel)s>non spécifié</option>

View File

@ -68,8 +68,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
import app import app
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import html_sidebar
from app.scodoc import imageresize
from app.scodoc import sco_import_etuds from app.scodoc import sco_import_etuds
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_archives_etud from app.scodoc import sco_archives_etud
@ -87,12 +85,9 @@ from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_edit from app.scodoc import sco_groups_edit
from app.scodoc import sco_groups_exports
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_logos
from app.scodoc import sco_news
from app.scodoc import sco_page_etud from app.scodoc import sco_page_etud
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_permissions
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_photos from app.scodoc import sco_photos
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
@ -364,6 +359,12 @@ sco_publish(
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish(
"/groups_export_annotations",
sco_groups_exports.groups_export_annotations,
Permission.ScoView,
)
@bp.route("/groups_view") @bp.route("/groups_view")
@scodoc @scodoc

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.2a-66" SCOVERSION = "9.2a-70"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"