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

This commit is contained in:
Emmanuel Viennet 2022-03-03 21:24:32 +01:00
commit aa09a7ef07
64 changed files with 1238 additions and 465 deletions

View File

@ -21,6 +21,7 @@ from flask import g
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_codes_parcours import ParcoursDUT, ParcoursDUTMono
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -53,7 +54,7 @@ class BonusSport:
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs). etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
""" """
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen reste None) # En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen est ajusté pour le prendre en compte)
classic_use_bonus_ues = False classic_use_bonus_ues = False
# Attributs virtuels: # Attributs virtuels:
@ -198,23 +199,29 @@ 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):
"""calcul du bonus """calcul du bonus
sem_modimpl_moys_inscrits: les notes de sport sem_modimpl_moys_inscrits: les notes de sport
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus) En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
modimpl_coefs_etuds_no_nan: En classic: ndarray (nb_etuds, nb_mod_sport)
modimpl_coefs_etuds_no_nan: même shape, les coefs.
""" """
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,
@ -227,13 +234,27 @@ class BonusSportAdditif(BonusSport):
else: # necessaire pour éviter bonus négatifs ! else: # necessaire pour éviter bonus négatifs !
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr) bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
self.bonus_additif(bonus_moy_arr)
def bonus_additif(self, bonus_moy_arr: np.array):
"Set bonus_ues et bonus_moy_gen"
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus) # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues: if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale # Bonus sur les UE et None sur moyenne générale
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)] ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame( self.bonus_ues = pd.DataFrame(
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
) )
elif self.classic_use_bonus_ues:
# Formations classiques apppliquant le bonus sur les UEs
# ici bonus_moy_arr = ndarray 1d nb_etuds
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues_idx)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
else: else:
# Bonus sur la moyenne générale seulement # Bonus sur la moyenne générale seulement
self.bonus_moy_gen = pd.Series( self.bonus_moy_gen = pd.Series(
@ -284,6 +305,7 @@ class BonusSportMultiplicatif(BonusSport):
class BonusDirect(BonusSportAdditif): class BonusDirect(BonusSportAdditif):
"""Bonus direct: les points sont directement ajoutés à la moyenne générale. """Bonus direct: les points sont directement ajoutés à la moyenne générale.
Les coefficients sont ignorés: tous les points de bonus sont sommés. Les coefficients sont ignorés: tous les points de bonus sont sommés.
(rappel: la note est ramenée sur 20 avant application). (rappel: la note est ramenée sur 20 avant application).
""" """
@ -294,47 +316,108 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0 proportion_point = 1.0
class BonusAnnecy(BonusSport): class BonusAisneStQuentin(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport), règle IUT d'Annecy. """Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin
Il peut y avoir plusieurs modules de bonus.
Prend pour chaque étudiant la meilleure de ses notes bonus et <p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
ajoute à chaque UE : de l'Université de St Quentin non rattachés à une unité d'enseignement.
0.05 point si >=10, </p>
0.1 point si >=12, <ul>
0.15 point si >=14, <li>Si la note est >= 10 et < 12.1, bonus de 0.1 point</li>
0.2 point si >=16, <li>Si la note est >= 12.1 et < 14.1, bonus de 0.2 point</li>
0.25 point si >=18. <li>Si la note est >= 14.1 et < 16.1, bonus de 0.3 point</li>
<li>Si la note est >= 16.1 et < 18.1, bonus de 0.4 point</li>
<li>Si la note est >= 18.1, bonus de 0.5 point</li>
</ul>
<p>
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE).
</p>
""" """
name = "bonus_iut_annecy" name = "bonus_iutstq"
displayed_name = "IUT d'Annecy" displayed_name = "IUT de Saint-Quentin"
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):
"""calcul du bonus""" """calcul du bonus"""
# if math.prod(sem_modimpl_moys_inscrits.shape) == 0: if 0 in sem_modimpl_moys_inscrits.shape:
# return # no etuds or no mod sport # pas d'étudiants ou pas d'UE ou pas de module...
# Prend la note de chaque modimpl, sans considération d'UE return
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc # Calcule moyenne pondérée des notes de sport:
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0] with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic bonus_moy_arr = np.sum(
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
bonus = np.zeros(note_bonus_max.shape) ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus[note_bonus_max >= 18.0] = 0.25 np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus[note_bonus_max >= 16.0] = 0.20
bonus[note_bonus_max >= 14.0] = 0.15
bonus[note_bonus_max >= 12.0] = 0.10
bonus[note_bonus_max >= 10.0] = 0.05
# Bonus moyenne générale et sur les UE bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float) bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)] bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
nb_ues_no_bonus = len(ues_idx) bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
self.bonus_ues = pd.DataFrame( bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
np.stack([bonus] * nb_ues_no_bonus, axis=1), bonus_moy_arr[bonus_moy_arr >= 10] = 0.1
columns=ues_idx,
index=self.etuds_idx, self.bonus_additif(bonus_moy_arr)
dtype=float,
)
class BonusAmiens(BonusSportAdditif):
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
sur toutes les moyennes d'UE.
"""
name = "bonus_amiens"
displayed_name = "IUT d'Amiens"
seuil_moy_gen = 0.0 # tous les points sont comptés
proportion_point = 1e10
bonus_max = 0.1
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
# Finalement ils n'en veulent pas.
# class BonusAnnecy(BonusSport):
# """Calcul bonus modules optionnels (sport), règle IUT d'Annecy.
# Il peut y avoir plusieurs modules de bonus.
# Prend pour chaque étudiant la meilleure de ses notes bonus et
# ajoute à chaque UE :<br>
# 0.05 point si >=10,<br>
# 0.1 point si >=12,<br>
# 0.15 point si >=14,<br>
# 0.2 point si >=16,<br>
# 0.25 point si >=18.
# """
# name = "bonus_iut_annecy"
# displayed_name = "IUT d'Annecy"
# def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
# """calcul du bonus"""
# # if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
# # return # no etuds or no mod sport
# # Prend la note de chaque modimpl, sans considération d'UE
# if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
# sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
# note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
# bonus = np.zeros(note_bonus_max.shape)
# bonus[note_bonus_max >= 10.0] = 0.05
# bonus[note_bonus_max >= 12.0] = 0.10
# bonus[note_bonus_max >= 14.0] = 0.15
# bonus[note_bonus_max >= 16.0] = 0.20
# bonus[note_bonus_max >= 18.0] = 0.25
# # Bonus moyenne générale et sur les UE
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
# ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
# nb_ues_no_bonus = len(ues_idx)
# self.bonus_ues = pd.DataFrame(
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
# columns=ues_idx,
# index=self.etuds_idx,
# dtype=float,
# )
class BonusBethune(BonusSportMultiplicatif): class BonusBethune(BonusSportMultiplicatif):
@ -373,26 +456,150 @@ class BonusBezier(BonusSportAdditif):
class BonusBordeaux1(BonusSportMultiplicatif): class BonusBordeaux1(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1,
et UE. sur moyenne générale et UEs.
<p>
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement. de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
</p><p>
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un % Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
qui augmente la moyenne de chaque UE et la moyenne générale. qui augmente la moyenne de chaque UE et la moyenne générale.<br>
Formule : le % = points>moyenne / 2 Formule : pourcentage = (points au dessus de 10) / 2
</p><p>
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale. Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
</p>
""" """
name = "bonus_iutBordeaux1" name = "bonus_iutBordeaux1"
displayed_name = "IUT de Bordeaux 1" displayed_name = "IUT de Bordeaux"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 10.0 seuil_moy_gen = 10.0
amplitude = 0.005 amplitude = 0.005
# Exactement le même que Bordeaux:
class BonusBrest(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Brest,
sur moyenne générale et UEs.
<p>
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université (sport, théâtre) non rattachés à une unité d'enseignement.
</p><p>
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
qui augmente la moyenne de chaque UE et la moyenne générale.<br>
Formule : pourcentage = (points au dessus de 10) / 2
</p><p>
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
</p>
"""
name = "bonus_iut_brest"
displayed_name = "IUT de Brest"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 10.0
amplitude = 0.005
class BonusCachan1(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1.
<ul>
<li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
bonifie les moyennes d'UE (<b>sauf l'UE41 dont le code est UE41_E</b>) à raison
de <em>bonus = (option - 10)/10</em>.
</li>
<li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
les moyennes d'UE à raison de <em>bonus = (option - 10)*5%</em>.</li>
</ul>
"""
name = "bonus_cachan1"
displayed_name = "IUT de Cachan 1"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.05
classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant le type de formation"""
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
else: # --- DUT
# pareil mais proportion différente et exclusion d'une UE
proportion_point = 0.1
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
# Pas de bonus sur la ou les ue de code "UE41_E"
ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"]
for ue in ue_exclues:
self.bonus_ues[ue.id] = 0.0
class BonusCalais(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT LCO.
Les étudiants de l'IUT LCO peuvent suivre des enseignements optionnels non
rattachés à une unité d'enseignement. Les points au-dessus de 10
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
dans la limite de 10 points. 6% de ces points cumulés s'ajoutent :
<ul>
<li><b>en DUT</b> à la moyenne générale du semestre déjà obtenue par l'étudiant.
<li><b>en BUT et LP</b> à la moyenne des UE dont l'acronyme fini par <b>BS</b> (ex : UE2.1BS, UE32BS)
</ul>
"""
name = "bonus_calais"
displayed_name = "IUT du Littoral"
bonus_max = 0.6
seuil_moy_gen = 10.0 # au dessus de 10
proportion_point = 0.06 # 6%
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
parcours = self.formsemestre.formation.get_parcours()
# Variantes de DUT ?
if (
isinstance(parcours, ParcoursDUT)
or parcours.TYPE_PARCOURS == ParcoursDUTMono.TYPE_PARCOURS
): # DUT
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
else:
self.classic_use_bonus_ues = True # pour les LP
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_sans_bs = [
ue for ue in ues if ue.acronyme[-2:].upper() != "BS"
] # les 2 derniers cars forcés en majus
for ue in ues_sans_bs:
self.bonus_ues[ue.id] = 0.0
class BonusColmar(BonusSportAdditif): class BonusColmar(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Colmar. """Calcul bonus modules optionnels (sport, culture), règle IUT Colmar.
@ -417,19 +624,21 @@ class BonusColmar(BonusSportAdditif):
class BonusGrenobleIUT1(BonusSportMultiplicatif): class BonusGrenobleIUT1(BonusSportMultiplicatif):
"""Bonus IUT1 de Grenoble """Bonus IUT1 de Grenoble
<p>
À compter de sept. 2021: À compter de sept. 2021:
La note de sport est sur 20, et on calcule une bonification (en %) La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
la formule : bonification (en %) = (note-10)*0,5. la formule : bonification (en %) = (note-10)*0,5.
</p><p>
Bonification qui ne s'applique que si la note est >10. <em>La bonification ne s'applique que si la note est supérieure à 10.</em>
</p><p>
(Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif) (Une note de 10 donne donc 0% de bonif, et une note de 20 : 5% de bonif)
</p><p>
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20). Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
Chaque point correspondait à 0.25% d'augmentation de la moyenne Chaque point correspondait à 0.25% d'augmentation de la moyenne
générale. générale.
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%. Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
</p>
""" """
name = "bonus_iut1grenoble_2017" name = "bonus_iut1grenoble_2017"
@ -456,15 +665,18 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
class BonusLaRochelle(BonusSportAdditif): class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle. """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point. <ul>
Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette <li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
note sur la moyenne générale du semestre (ou sur les UE en BUT). <li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li>
</ul>
""" """
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):
@ -483,16 +695,17 @@ class BonusLeHavre(BonusSportMultiplicatif):
class BonusLeMans(BonusSportAdditif): class BonusLeMans(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans. """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières <p>Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés. optionnelles sont cumulés.
</p>
<ul>
<li>En BUT: la moyenne de chacune des UE du semestre est augmentée de
2% du cumul des points de bonus;</li>
<li>En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
En BUT: la moyenne de chacune des UE du semestre est augmentée de </li>
2% du cumul des points de bonus, </ul>
<p>Dans tous les cas, le bonus est dans la limite de 0,5 point.</p>
En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
Dans tous les cas, le bonus est dans la limite de 0,5 point.
""" """
name = "bonus_iutlemans" name = "bonus_iutlemans"
@ -516,12 +729,13 @@ class BonusLeMans(BonusSportAdditif):
class BonusLille(BonusSportAdditif): class BonusLille(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq """Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq
Les étudiants de l'IUT peuvent suivre des enseignements optionnels <p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement. de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement.
</p><p>
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant. s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
</p>
""" """
name = "bonus_lille" name = "bonus_lille"
@ -573,17 +787,19 @@ class BonusMulhouse(BonusSportAdditif):
class BonusNantes(BonusSportAdditif): class BonusNantes(BonusSportAdditif):
"""IUT de Nantes (Septembre 2018) """IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification <p>Nous avons différents types de bonification
(sport, culture, engagement citoyen). (sport, culture, engagement citoyen).
</p><p>
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
la bonification totale ne doit pas excéder les 0,5 point. la bonification totale ne doit pas excéder les 0,5 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications. Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
</p><p>
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura des modules Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura
pour chaque activité (Sport, Associations, ...) des modules pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20,
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale) mais en fait ce sera la valeur de la bonification: entrer 0,1/20 signifiera
un bonus de 0,1 point la moyenne générale).
</p>
""" """
name = "bonus_nantes" name = "bonus_nantes"
@ -604,7 +820,8 @@ class BonusRoanne(BonusSportAdditif):
displayed_name = "IUT de Roanne" displayed_name = "IUT de Roanne"
seuil_moy_gen = 0.0 seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points bonus_max = 0.6 # plafonnement à 0.6 points
apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP classic_use_bonus_ues = True # sur les UE, même en DUT et LP
proportion_point = 1
class BonusStDenis(BonusSportAdditif): class BonusStDenis(BonusSportAdditif):
@ -627,13 +844,14 @@ class BonusStDenis(BonusSportAdditif):
class BonusTours(BonusDirect): class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours. """Calcul bonus sport & culture IUT Tours.
Les notes des UE bonus (ramenées sur 20) sont sommées <p>Les notes des UE bonus (ramenées sur 20) sont sommées
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale, et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
soit pour le BUT à chaque moyenne d'UE. soit pour le BUT à chaque moyenne d'UE.
</p><p>
Attention: en GEII, facteur 1/40, ailleurs facteur 1. <em>Attention: en GEII, facteur 1/40, ailleurs facteur 1.</em>
</p><p>
Le bonus total est limité à 1 point. Le bonus total est limité à 1 point.
</p>
""" """
name = "bonus_tours" name = "bonus_tours"
@ -658,11 +876,13 @@ class BonusVilleAvray(BonusSport):
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement. de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
Si la note est >= 10 et < 12, bonus de 0.1 point <ul>
Si la note est >= 12 et < 16, bonus de 0.2 point <li>Si la note est >= 10 et < 12, bonus de 0.1 point</li>
Si la note est >= 16, bonus de 0.3 point <li>Si la note est >= 12 et < 16, bonus de 0.2 point</li>
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par <li>Si la note est >= 16, bonus de 0.3 point</li>
l'étudiant. </ul>
<p>Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant.</p>
""" """
name = "bonus_iutva" name = "bonus_iutva"
@ -670,21 +890,21 @@ class BonusVilleAvray(BonusSport):
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):
"""calcul du bonus""" """calcul du bonus"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
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
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 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 >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
# Bonus moyenne générale, et 0 sur les UE self.bonus_additif(bonus_moy_arr)
self.bonus_moy_gen = pd.Series(bonus_moy_arr, index=self.etuds_idx, dtype=float)
if self.bonus_max is not None:
# Seuil: bonus (sur moy. gen.) limité à bonus_max points
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_max)
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
class BonusIUTV(BonusSportAdditif): class BonusIUTV(BonusSportAdditif):
@ -700,7 +920,7 @@ class BonusIUTV(BonusSportAdditif):
name = "bonus_iutv" name = "bonus_iutv"
displayed_name = "IUT de Villetaneuse" displayed_name = "IUT de Villetaneuse"
pass # oui, c'ets le bonus par défaut pass # oui, c'est le bonus par défaut
def get_bonus_class_dict(start=BonusSport, d=None): def get_bonus_class_dict(start=BonusSport, d=None):

50
app/comp/moy_mat.py Normal file
View File

@ -0,0 +1,50 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Calcul des moyennes de matières
"""
# C'est un recalcul (optionnel) effectué _après_ le calcul standard.
import numpy as np
import pandas as pd
from app.comp import moy_ue
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
def compute_mat_moys_classic(
formsemestre: FormSemestre,
sem_matrix: np.array,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
) -> dict:
"""Calcul des moyennes par matières.
Result: dict, { matiere_id : Series, index etudid }
"""
modimpls_std = [
m
for m in formsemestre.modimpls_sorted
if (m.module.module_type == ModuleType.STANDARD)
and (m.module.ue.type != UE_SPORT)
]
matiere_ids = {m.module.matiere.id for m in modimpls_std}
matiere_moy = {} # { matiere_id : moy pd.Series, index etudid }
for matiere_id in matiere_ids:
modimpl_mask = np.array(
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
)
etud_moy_mat = moy_ue.compute_mat_moys_classic(
sem_matrix=sem_matrix,
modimpl_inscr_df=modimpl_inscr_df,
modimpl_coefs=modimpl_coefs,
modimpl_mask=modimpl_mask,
)
matiere_moy[matiere_id] = etud_moy_mat
return matiere_moy

View File

@ -335,15 +335,17 @@ class ModuleImplResultsAPC(ModuleImplResults):
notes_rat / (eval_rat.note_max / 20.0), notes_rat / (eval_rat.note_max / 20.0),
np.nan, np.nan,
) )
# "Étend" le rattrapage sur les UE: la note de rattrapage est la même
# pour toutes les UE mais ne remplace que là où elle est supérieure
notes_rat_ues = np.stack([notes_rat] * nb_ues, axis=1)
# prend le max # prend le max
etuds_use_rattrapage = notes_rat > etuds_moy_module etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
etuds_moy_module = np.where( etuds_moy_module = np.where(
etuds_use_rattrapage[:, np.newaxis], etuds_use_rattrapage, notes_rat_ues, etuds_moy_module
np.tile(notes_rat[:, np.newaxis], nb_ues),
etuds_moy_module,
) )
# Serie indiquant que l'étudiant utilise une note de rattarage sur l'une des UE:
self.etuds_use_rattrapage = pd.Series( self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index etuds_use_rattrapage.any(axis=1), index=self.evals_notes.index
) )
self.etuds_moy_module = pd.DataFrame( self.etuds_moy_module = pd.DataFrame(
etuds_moy_module, etuds_moy_module,
@ -359,6 +361,10 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
Les valeurs manquantes (évaluations sans coef vers des UE) sont Les valeurs manquantes (évaluations sans coef vers des UE) sont
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
(sauf pour module bonus, defaut à 1) (sauf pour module bonus, defaut à 1)
Si le module n'est pas une ressource ou une SAE, ne charge pas de poids
et renvoie toujours les poids par défaut.
Résultat: (evals_poids, liste de UEs du semestre sauf le sport) Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
""" """
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
@ -367,13 +373,17 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
ue_ids = [ue.id for ue in ues] ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations] evaluation_ids = [evaluation.id for evaluation in evaluations]
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for ue_poids in EvaluationUEPoids.query.join( if (
EvaluationUEPoids.evaluation modimpl.module.module_type == ModuleType.RESSOURCE
).filter_by(moduleimpl_id=moduleimpl_id): or modimpl.module.module_type == ModuleType.SAE
try: ):
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids for ue_poids in EvaluationUEPoids.query.join(
except KeyError as exc: EvaluationUEPoids.evaluation
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... ).filter_by(moduleimpl_id=moduleimpl_id):
try:
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
except KeyError as exc:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés: # Initialise poids non enregistrés:
default_poids = ( default_poids = (

View File

@ -30,8 +30,10 @@
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from flask import flash
def compute_sem_moys_apc(
def compute_sem_moys_apc_using_coefs(
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series: ) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants """Calcule les moyennes générales indicatives de tous les étudiants
@ -48,6 +50,28 @@ def compute_sem_moys_apc(
return moy_gen return moy_gen
def compute_sem_moys_apc_using_ects(
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
ects: liste de floats ou None, 1 par UE
Result: panda Series, index etudid, valeur float (moyenne générale)
"""
try:
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
except TypeError:
if None in ects:
flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""")
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
else:
raise
return moy_gen
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series): def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur """Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos. numérique) en tenant compte des ex-aequos.

View File

@ -27,7 +27,6 @@
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT) """Fonctions de calcul des moyennes d'UE (classiques ou BUT)
""" """
from re import X
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -218,21 +217,25 @@ def compute_ue_moys_apc(
ues: list, ues: list,
modimpl_inscr_df: pd.DataFrame, modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame,
modimpl_mask: np.array,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Calcul de la moyenne d'UE en mode APC (BUT). """Calcul de la moyenne d'UE en mode APC (BUT).
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
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici] ERR erreur dans une formule utilisateurs (pas gérées ici).
sem_cube: notes moyennes aux modules sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs) ndarray (etuds x modimpls x UEs)
(floats avec des NaN) (floats avec des NaN)
etuds : liste des étudiants (dim. 0 du cube) etuds : liste des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube) modimpls : liste des module_impl (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube) ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
(utilisé pour éliminer les bonus, et pourra servir à cacluler
sur des sous-ensembles de modules)
Résultat: DataFrame columns UE (sans bonus), rows etudid Résultat: DataFrame columns UE (sans bonus), rows etudid
""" """
@ -249,7 +252,8 @@ def compute_ue_moys_apc(
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values # Met à zéro tous les coefs des modules non sélectionnés dans le masque:
modimpl_coefs = np.where(modimpl_mask, modimpl_coefs_df.values, 0.0)
# Duplique les inscriptions sur les UEs non bonus: # Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2) modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
@ -290,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
@ -359,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)
@ -404,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

@ -14,7 +14,7 @@ from app import log
from app.comp import moy_ue, moy_sem, inscr_mod from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig, formsemestre
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
@ -56,14 +56,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
# modimpl_coefs_df.columns.get_loc(modimpl.id) # modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
# Elimine les coefs des modimpl bonus sports: # Masque de tous les modules _sauf_ les bonus (sport)
modimpls_sport = [ modimpls_mask = [
modimpl modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.type == UE_SPORT
] ]
for modimpl in modimpls_sport:
self.modimpl_coefs_df[modimpl.id] = 0
self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube, self.sem_cube,
@ -72,10 +69,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.ues, self.ues,
self.modimpl_inscr_df, self.modimpl_inscr_df,
self.modimpl_coefs_df, self.modimpl_coefs_df,
modimpls_mask,
) )
# Les coefficients d'UE ne sont pas utilisés en APC # Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame( self.etud_coef_ue_df = pd.DataFrame(
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns 0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
) )
# --- Modules de MALUS sur les UEs # --- Modules de MALUS sur les UEs
@ -85,7 +83,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue -= self.malus self.etud_moy_ue -= self.malus
# --- Bonus Sport & Culture # --- Bonus Sport & Culture
if len(modimpls_sport) > 0: if not all(modimpls_mask): # au moins un module bonus
bonus_class = ScoDocSiteConfig.get_bonus_sport_class() bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None: if bonus_class is not None:
bonus: BonusSport = bonus_class( bonus: BonusSport = bonus_class(
@ -100,13 +98,20 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.bonus_ues = bonus.get_bonus_ues() self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None: if self.bonus_ues is not None:
self.etud_moy_ue += self.bonus_ues # somme les dataframes self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Clippe toutes les moyennes d'UE dans [0,20]
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Moyenne générale indicative: # Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative) # donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc( # self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
self.etud_moy_ue, self.modimpl_coefs_df # self.etud_moy_ue, self.modimpl_coefs_df
# )
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
self.etud_moy_ue,
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
formation_id=self.formsemestre.formation_id,
) )
# --- UE capitalisées # --- UE capitalisées
self.apply_capitalisation() self.apply_capitalisation()

View File

@ -15,7 +15,7 @@ from flask import g, url_for
from app import db from app import db
from app import log from app import log
from app.comp import moy_mod, moy_ue, inscr_mod from app.comp import moy_mat, moy_mod, moy_ue, inscr_mod
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
@ -24,6 +24,7 @@ from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_preferences
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -60,7 +61,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
) )
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs = np.array( self.modimpl_coefs = np.array(
[m.module.coefficient for m in self.formsemestre.modimpls_sorted] [m.module.coefficient or 0.0 for m in self.formsemestre.modimpls_sorted]
) )
self.modimpl_idx = { self.modimpl_idx = {
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted) m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
@ -113,17 +114,34 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.etud_moy_ue += self.bonus_ues # somme les dataframes self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
bonus_mg = bonus.get_bonus_moy_gen() bonus_mg = bonus.get_bonus_moy_gen()
if bonus_mg is not None: if bonus_mg is None and self.bonus_ues is not None:
# pas de bonus explicite sur la moyenne générale
# on l'ajuste pour refléter les modifs d'UE, à l'aide des coefs d'UE.
bonus_mg = (self.etud_coef_ue_df * self.bonus_ues).sum(
axis=1
) / self.etud_coef_ue_df.sum(axis=1)
self.etud_moy_gen += bonus_mg self.etud_moy_gen += bonus_mg
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True) elif bonus_mg is not None:
# compat nt, utilisé pour l'afficher sur les bulletins: # Applique le bonus moyenne générale renvoyé
self.bonus = bonus_mg self.etud_moy_gen += bonus_mg
# compat nt, utilisé pour l'afficher sur les bulletins:
self.bonus = bonus_mg
# --- UE capitalisées # --- UE capitalisées
self.apply_capitalisation() self.apply_capitalisation()
# Clippe toutes les moyennes dans [0,20]
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
# --- Classements: # --- Classements:
self.compute_rangs() self.compute_rangs()
# --- En option, moyennes par matières
if sco_preferences.get_preference("bul_show_matieres", self.formsemestre.id):
self.compute_moyennes_matieres()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl """La moyenne de l'étudiant dans le moduleimpl
Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM) Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
@ -149,6 +167,16 @@ class ResultatsSemestreClassic(NotesTableCompat):
), ),
} }
def compute_moyennes_matieres(self):
"""Calcul les moyennes par matière. Doit être appelée au besoin, en fin de compute."""
self.moyennes_matieres = moy_mat.compute_mat_moys_classic(
self.formsemestre,
self.sem_matrix,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
)
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float: def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant. """Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la N'est utilisé que pour l'injection des UE capitalisées dans la

View File

@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from flask import g, flash, url_for
from app import log from app import log
from app.comp.aux_stats import StatsMoyenne from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl from app.models import FormSemestre, FormSemestreUECoef
from app.models import FormSemestreUECoef from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer # Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis): # - ce qui est caché de façon persistente (via redis):
@ -39,6 +43,7 @@ class ResultatsSemestre(ResultatsCache):
"modimpl_inscr_df", "modimpl_inscr_df",
"modimpls_results", "modimpls_results",
"etud_coef_ue_df", "etud_coef_ue_df",
"moyennes_matieres",
) )
def __init__(self, formsemestre: FormSemestre): def __init__(self, formsemestre: FormSemestre):
@ -57,6 +62,8 @@ class ResultatsSemestre(ResultatsCache):
self.etud_coef_ue_df = None self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)""" """coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None self.validations = None
self.moyennes_matieres = {}
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
def compute(self): def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes" "Charge les notes et inscriptions et calcule toutes les moyennes"
@ -165,7 +172,6 @@ class ResultatsSemestre(ResultatsCache):
""" """
# Supposant qu'il y a peu d'UE capitalisées, # Supposant qu'il y a peu d'UE capitalisées,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée. # on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
# return # XXX XXX XXX
if not self.validations: if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre) self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees ue_capitalisees = self.validations.ue_capitalisees
@ -184,10 +190,12 @@ class ResultatsSemestre(ResultatsCache):
sum_coefs_ue = 0.0 sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues(): for ue in self.formsemestre.query_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id) ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap and ue_cap["is_capitalized"]: if ue_cap is None:
continue
if ue_cap["is_capitalized"]:
recompute_mg = True recompute_mg = True
coef = ue_cap["coef_ue"] coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]): if not np.isnan(ue_cap["moy"]) and coef:
sum_notes_ue += ue_cap["moy"] * coef sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef sum_coefs_ue += coef
@ -195,13 +203,25 @@ class ResultatsSemestre(ResultatsCache):
# On doit prendre en compte une ou plusieurs UE capitalisées # On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale # et donc recalculer la moyenne générale
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
# Ajoute le bonus sport
if self.bonus is not None and self.bonus[etudid]:
self.etud_moy_gen[etudid] += self.bonus[etudid]
self.etud_moy_gen[etudid] = max(
0.0, min(self.etud_moy_gen[etudid], 20.0)
)
def _get_etud_ue_cap(self, etudid, ue): def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
"""""" """Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
Résultat:
Si pas capitalisée: None
Si capitalisée: un dict, avec les colonnes de validation.
"""
capitalisations = self.validations.ue_capitalisees.loc[etudid] capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame): if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code] ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty: if ue_cap.empty:
return None
if isinstance(ue_cap, pd.DataFrame):
# si plusieurs fois capitalisée, prend le max # si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax() cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx] ue_cap = ue_cap.iloc[cap_idx]
@ -209,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code: if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations ue_cap = capitalisations
else: else:
ue_cap = None return None
return ue_cap # converti la Series en dict, afin que les np.int64 reviennent en int
return ue_cap.to_dict()
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict: def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant. """L'état de l'UE pour cet étudiant.
@ -238,22 +259,45 @@ class ResultatsSemestre(ResultatsCache):
cur_moy_ue = self.etud_moy_ue[ue_id][etudid] cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue moy_ue = cur_moy_ue
is_capitalized = False # si l'UE prise en compte est une UE capitalisée is_capitalized = False # si l'UE prise en compte est une UE capitalisée
was_capitalized = ( # s'il y a precedemment une UE capitalisée (pas forcement meilleure):
False # s'il y a precedemment une UE capitalisée (pas forcement meilleure) was_capitalized = False
)
if etudid in self.validations.ue_capitalisees.index: if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue) ue_cap = self._get_etud_ue_cap(etudid, ue)
if ( if ue_cap and not np.isnan(ue_cap["moy_ue"]):
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
was_capitalized = True was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"] moy_ue = ue_cap["moy_ue"]
is_capitalized = True is_capitalized = True
coef_ue = self.etud_coef_ue_df[ue_id][etudid] # Coef l'UE dans le semestre courant:
if self.is_apc:
# utilise les ECTS comme coef.
coef_ue = ue.ects
else:
# formations classiques
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
if self.is_apc:
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
coef_ue = ue_capitalized.ects
if coef_ue is None:
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
raise ScoValueError(
f"""L'UE capitalisée {ue_capitalized.acronyme}
du semestre {orig_sem.titre_annee()}
n'a pas d'indication d'ECTS.
Corrigez ou faite corriger le programme
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
"""
)
else:
# Coefs de l'UE capitalisée en formation classique:
# va chercher le coef dans le semestre d'origine
coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue(
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
)
return { return {
"is_capitalized": is_capitalized, "is_capitalized": is_capitalized,
@ -375,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre):
"""Stats (moy/min/max) sur la moyenne générale""" """Stats (moy/min/max) sur la moyenne générale"""
return StatsMoyenne(self.etud_moy_gen) return StatsMoyenne(self.etud_moy_gen)
def get_ues_stat_dict(self, filter_sport=False): # was get_ues() def get_ues_stat_dict(
self, filter_sport=False, check_apc_ects=True
) -> list[dict]: # was get_ues()
"""Liste des UEs, ordonnée par numero. """Liste des UEs, ordonnée par numero.
Si filter_sport, retire les UE de type SPORT. Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE } Résultat: liste de dicts { champs UE U stats moyenne UE }
""" """
ues = [] ues = self.formsemestre.query_ues(with_sport=not filter_sport)
for ue in self.formsemestre.query_ues(with_sport=not filter_sport): ues_dict = []
for ue in ues:
d = ue.to_dict() d = ue.to_dict()
if ue.type != UE_SPORT: if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id] moys = self.etud_moy_ue[ue.id]
else: else:
moys = None moys = None
d.update(StatsMoyenne(moys).to_dict()) d.update(StatsMoyenne(moys).to_dict())
ues.append(d) ues_dict.append(d)
return ues if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
g.checked_apc_ects = True
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
flash(
"""Calcul moyenne générale impossible: ECTS des UE manquants !""",
category="danger",
)
return ues_dict
def get_modimpls_dict(self, ue_id=None) -> list[dict]: def get_modimpls_dict(self, ue_id=None) -> list[dict]:
"""Liste des modules pour une UE (ou toutes si ue_id==None), """Liste des modules pour une UE (ou toutes si ue_id==None),
@ -508,10 +562,15 @@ class NotesTableCompat(ResultatsSemestre):
return "" return ""
return ins.etat return ins.etat
def get_etud_mat_moy(self, matiere_id, etudid): def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" """moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
# non supporté en 9.2 if not self.moyennes_matieres:
return "na" return "nd"
return (
self.moyennes_matieres[matiere_id].get(etudid, "-")
if matiere_id in self.moyennes_matieres
else "-"
)
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl """La moyenne de l'étudiant dans le moduleimpl

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

@ -193,7 +193,7 @@ def scodoc7func(func):
# necessary for db ids and boolean values # necessary for db ids and boolean values
try: try:
v = int(v) v = int(v)
except ValueError: except (ValueError, TypeError):
pass pass
pos_arg_values.append(v) pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values) # current_app.logger.info("pos_arg_values=%s" % pos_arg_values)

View File

@ -30,17 +30,15 @@ Formulaires configuration logos
Contrib @jmp, dec 21 Contrib @jmp, dec 21
""" """
import re
from flask import flash, url_for, redirect, render_template from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField, FormField, validators, FieldList from wtforms import SubmitField, FormField, validators, FieldList
from wtforms import ValidationError
from wtforms.fields.simple import StringField, HiddenField from wtforms.fields.simple import StringField, HiddenField
from app import AccessDenied
from app.models import Departement from app.models import Departement
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos, html_sco_header from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import ( from app.scodoc.sco_config_actions import (
@ -49,10 +47,11 @@ from app.scodoc.sco_config_actions import (
LogoInsert, LogoInsert,
) )
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_logos import find_logo from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [] JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -111,6 +110,15 @@ def dept_key_to_id(dept_key):
return dept_key return dept_key
def logo_name_validator(message=None):
def validate_logo_name(form, field):
name = field.data if field.data else ""
if not scu.is_valid_filename(name):
raise ValidationError(message)
return validate_logo_name
class AddLogoForm(FlaskForm): class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)""" """Formulaire permettant l'ajout d'un logo (dans un département)"""
@ -118,11 +126,7 @@ class AddLogoForm(FlaskForm):
name = StringField( name = StringField(
label="Nom", label="Nom",
validators=[ validators=[
validators.regexp( logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
r"^[a-zA-Z0-9-_]*$",
re.IGNORECASE,
"Ne doit comporter que lettres, chiffres, _ ou -",
),
validators.Length( validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères" max=20, message="Un nom ne doit pas dépasser 20 caractères"
), ),
@ -373,11 +377,11 @@ def config_logos():
if action: if action:
action.execute() action.execute()
flash(action.message) flash(action.message)
return redirect( return redirect(url_for("scodoc.configure_logos"))
url_for( else:
"scodoc.configure_logos", if not form.validate():
) scu.flash_errors(form)
)
return render_template( return render_template(
"config_logos.html", "config_logos.html",
scodoc_dept=None, scodoc_dept=None,

View File

@ -55,6 +55,9 @@ def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement" "Create new departement"
from app.models import ScoPreference from app.models import ScoPreference
existing = Departement.query.filter_by(acronym=acronym).count()
if existing:
raise ValueError(f"acronyme {acronym} déjà existant")
departement = Departement(acronym=acronym, visible=visible) departement = Departement(acronym=acronym, visible=visible)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement) p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1) db.session.add(p1)

View File

@ -104,6 +104,11 @@ class FormSemestre(db.Model):
lazy=True, lazy=True,
backref=db.backref("formsemestres", lazy=True), backref=db.backref("formsemestres", lazy=True),
) )
partitions = db.relationship(
"Partition",
backref=db.backref("formsemestre", lazy=True),
lazy="dynamic",
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes # Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives # ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True) scodoc7_id = db.Column(db.Text(), nullable=True)
@ -201,7 +206,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

@ -31,6 +31,11 @@ class Partition(db.Model):
show_in_lists = db.Column( show_in_lists = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" db.Boolean(), nullable=False, default=True, server_default="true"
) )
groups = db.relationship(
"GroupDescr",
backref=db.backref("partition", lazy=True),
lazy="dynamic",
)
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(Partition, self).__init__(**kwargs) super(Partition, self).__init__(**kwargs)
@ -42,6 +47,9 @@ class Partition(db.Model):
else: else:
self.numero = 1 self.numero = 1
def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">"""
class GroupDescr(db.Model): class GroupDescr(db.Model):
"""Description d'un groupe d'une partition""" """Description d'un groupe d'une partition"""
@ -55,6 +63,11 @@ class GroupDescr(db.Model):
# "A", "C2", ... (NULL for 'all'): # "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN)) group_name = db.Column(db.String(GROUPNAME_STR_LEN))
def __repr__(self):
return (
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
)
group_membership = db.Table( group_membership = db.Table(
"group_membership", "group_membership",

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls """ScoDoc models: moduleimpls
""" """
import pandas as pd import pandas as pd
import flask_sqlalchemy
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
) )
@classmethod @classmethod
def nb_inscriptions_dans_ue( def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int: ) -> flask_sqlalchemy.BaseQuery:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit""" """moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter( return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid, ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id, ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id, ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id, ModuleImpl.module_id == Module.id,
Module.ue_id == ue_id, Module.ue_id == ue_id,
).count() )
@classmethod
def nb_inscriptions_dans_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()
@classmethod
def sum_coefs_modimpl_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> float:
"""Somme des coefficients des modules auxquels l'étudiant est inscrit
dans l'UE du semestre indiqué.
N'utilise que les coefficients, donc inadapté aux formations APC.
"""
return sum(
[
inscr.modimpl.module.coefficient
for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
]
)

View File

@ -54,13 +54,15 @@ class UniteEns(db.Model):
'EXTERNE' if self.is_external else ''})>""" 'EXTERNE' if self.is_external else ''})>"""
def to_dict(self): def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7""" """as a dict, with the same conversions as in ScoDoc7
(except ECTS: keep None)
"""
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
e["ue_id"] = self.id e["ue_id"] = self.id
e["numero"] = e["numero"] if e["numero"] else 0 e["numero"] = e["numero"] if e["numero"] else 0
e["ects"] = e["ects"] if e["ects"] else 0.0 e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e return e

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

@ -30,7 +30,7 @@
import html import html
from flask import g from flask import render_template
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -280,6 +280,9 @@ def sco_header(
if not no_side_bar: if not no_side_bar:
H.append(html_sidebar.sidebar()) H.append(html_sidebar.sidebar())
H.append("""<div id="gtrcontent">""") H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.html"))
# #
# Barre menu semestre: # Barre menu semestre:
H.append(formsemestre_page_title()) H.append(formsemestre_page_title())

View File

@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
from app import log from app import log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre, Identite
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
NAR, NAR,
RAT, RAT,
) )
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -454,6 +453,12 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid): def comp_elt_semestre(self, nt, decision, etudid):
"""Calcul résultat apo semestre""" """Calcul résultat apo semestre"""
if decision is None:
etud = Identite.query.get(etudid)
nomprenom = etud.nomprenom if etud else "(inconnu)"
raise ScoValueError(
f"decision absente pour l'étudiant {nomprenom} ({etudid})"
)
# resultat du semestre # resultat du semestre
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"]) decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid) note = nt.get_etud_moy_gen(etudid)

View File

@ -291,15 +291,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"] = {} I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {} I["matieres_modules_capitalized"] = {}
for ue in ues: for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ( if (
ModuleImplInscription.nb_inscriptions_dans_ue( ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"] formsemestre_id, etudid, ue["ue_id"]
) )
== 0 == 0
): ) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...} u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT: if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"]) u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
@ -315,13 +317,13 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs" u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
else: else:
u["cur_moy_ue_txt"] = "bonus de %.3g points" % x u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
if nt.bonus_ues is not None:
u["cur_moy_ue_txt"] += " (+ues)"
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
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
@ -1011,11 +1013,16 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id) intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
if intro_mail: if intro_mail:
hea = intro_mail % { try:
"nomprenom": etud["nomprenom"], hea = intro_mail % {
"dept": dept, "nomprenom": etud["nomprenom"],
"webmaster": webmaster, "dept": dept,
} "webmaster": webmaster,
}
except KeyError as e:
raise ScoValueError(
"format 'Message d'accompagnement' (bul_intro_mail) invalide, revoir les réglages dans les préférences"
)
else: else:
hea = "" hea = ""

View File

@ -284,7 +284,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
) )
with_col_moypromo = prefs["bul_show_moypromo"] with_col_moypromo = prefs["bul_show_moypromo"]
with_col_rang = prefs["bul_show_rangs"] with_col_rang = prefs["bul_show_rangs"]
with_col_coef = prefs["bul_show_coef"] with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"]
with_col_ects = prefs["bul_show_ects"] with_col_ects = prefs["bul_show_ects"]
colkeys = ["titre", "module"] # noms des colonnes à afficher colkeys = ["titre", "module"] # noms des colonnes à afficher
@ -409,7 +409,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
# Chaque UE: # Chaque UE:
for ue in I["ues"]: for ue in I["ues"]:
ue_type = None ue_type = None
coef_ue = ue["coef_ue_txt"] coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
ue_descr = ue["ue_descr_txt"] ue_descr = ue["ue_descr_txt"]
rowstyle = "" rowstyle = ""
plusminus = minuslink # plusminus = minuslink #
@ -592,7 +592,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"_titre_colspan": 2, "_titre_colspan": 2,
"rang": mod["mod_rang_txt"], # vide si pas option rang "rang": mod["mod_rang_txt"], # vide si pas option rang
"note": mod["mod_moy_txt"], "note": mod["mod_moy_txt"],
"coef": mod["mod_coef_txt"], "coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "",
"abs": mod.get( "abs": mod.get(
"mod_abs_txt", "" "mod_abs_txt", ""
), # absent si pas option show abs module ), # absent si pas option show abs module
@ -656,7 +656,9 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
eval_style = "" eval_style = ""
t = { t = {
"module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"], "module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"],
"coef": "<i>" + e["coef_txt"] + "</i>", "coef": ("<i>" + e["coef_txt"] + "</i>")
if prefs["bul_show_coef"]
else "",
"_hidden": hidden, "_hidden": hidden,
"_module_target": e["target_html"], "_module_target": e["target_html"],
# '_module_help' : , # '_module_help' : ,

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

@ -282,7 +282,7 @@ class TypeParcours(object):
return [ return [
ue_status ue_status
for ue_status in ues_status for ue_status in ues_status
if ue_status["coef_ue"] > 0 if ue_status["coef_ue"]
and isinstance(ue_status["moy"], float) and isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"]) and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
] ]
@ -587,7 +587,7 @@ class ParcoursILEPS(TypeParcours):
# SESSION_ABBRV = 'A' # A1, A2, ... # SESSION_ABBRV = 'A' # A1, A2, ...
COMPENSATION_UE = False COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB, ATJ)) UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE] ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE, UE_SPORT]
# Barre moy gen. pour validation semestre: # Barre moy gen. pour validation semestre:
BARRE_MOY = 10.0 BARRE_MOY = 10.0
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales") # Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")

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()
@ -186,18 +188,28 @@ def _send_db(ano_db_name):
log("uploading anonymized dump...") log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".dump", dump)} files = {"file": (ano_db_name + ".dump", dump)}
r = requests.post( try:
scu.SCO_DUMP_UP_URL, r = requests.post(
files=files, scu.SCO_DUMP_UP_URL,
data={ files=files,
"dept_name": sco_preferences.get_preference("DeptName"), data={
"serial": _get_scodoc_serial(), "dept_name": sco_preferences.get_preference("DeptName"),
"sco_user": str(current_user), "serial": _get_scodoc_serial(),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"], "sco_user": str(current_user),
"sco_version": sco_version.SCOVERSION, "sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_fullversion": scu.get_scodoc_version(), "sco_version": sco_version.SCOVERSION,
}, "sco_fullversion": scu.get_scodoc_version(),
) },
)
except requests.exceptions.ConnectionError as exc:
raise ScoValueError(
"""
Impossible de joindre le serveur d'assistance (scodoc.org).
Veuillez contacter le service informatique de votre établissement pour
corriger la configuration de ScoDoc. Dans la plupart des cas, il
s'agit d'un proxy mal configuré.
"""
) from exc
return r return r

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

@ -500,6 +500,13 @@ def module_edit(module_id=None):
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
if is_apc: if is_apc:
# ne conserve que la 1ere matière de chaque UE,
# et celle à laquelle ce module est rattaché
matieres = [
mat
for mat in matieres
if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id
]
mat_names = [ mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
] ]
@ -544,14 +551,18 @@ def module_edit(module_id=None):
# ne propose pas SAE et Ressources, sauf si déjà de ce type... # ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = ( module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
) | {a_module.module_type or scu.ModuleType.STANDARD} ) | {
scu.ModuleType(a_module.module_type)
if a_module.module_type
else scu.ModuleType.STANDARD
}
descr = [ descr = [
( (
"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
@ -690,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

@ -29,7 +29,7 @@
""" """
import flask import flask
from flask import url_for, render_template from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
input_formators={ input_formators={
"type": ndb.int_null_is_zero, "type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str, "is_external": ndb.bool_or_str,
"ects": ndb.float_null_is_null,
}, },
output_formators={ output_formators={
"numero": ndb.int_null_is_zero, "numero": ndb.int_null_is_zero,
@ -107,8 +108,6 @@ def ue_list(*args, **kw):
def do_ue_create(args): def do_ue_create(args):
"create an ue" "create an ue"
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check duplicates # check duplicates
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]}) ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
@ -117,6 +116,14 @@ def do_ue_create(args):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)""" (chaque UE doit avoir un acronyme unique dans la formation)"""
) )
if not "ue_code" in args:
# évite les conflits de code
while True:
cursor = db.session.execute("select notes_newid_ucod();")
code = cursor.fetchone()[0]
if UniteEns.query.filter_by(ue_code=code).count() == 0:
break
args["ue_code"] = code
# create # create
ue_id = _ueEditor.create(cnx, args) ue_id = _ueEditor.create(cnx, args)
@ -128,6 +135,8 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs() formation.invalidate_module_coefs()
# news # news
ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
sco_news.add( sco_news.add(
typ=sco_news.NEWS_FORM, typ=sco_news.NEWS_FORM,
@ -296,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",
}, },
@ -339,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float", "type": "float",
"title": "ECTS", "title": "ECTS",
"explanation": "nombre de crédits ECTS", "explanation": "nombre de crédits ECTS",
"allow_null": not is_apc, # ects requis en APC
}, },
), ),
( (
@ -462,8 +472,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"semestre_id": tf[2]["semestre_idx"], "semestre_id": tf[2]["semestre_idx"],
}, },
) )
flash("UE créée")
else: else:
do_ue_edit(tf[2]) do_ue_edit(tf[2])
flash("UE modifiée")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",
@ -601,7 +613,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
_add_ue_semestre_id(ues_externes, is_apc) _add_ue_semestre_id(ues_externes, is_apc)
ues.sort(key=lambda u: (u["semestre_id"], u["numero"])) ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"])) ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues) # Codes dupliqués (pour aider l'utilisateur)
seen = set()
duplicated_codes = {
ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"])
}
ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes]
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation) has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and has_perm_change # editable = (not locked) and has_perm_change
@ -664,11 +681,17 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if msg: if msg:
H.append('<p class="msg">' + msg + "</p>") H.append('<p class="msg">' + msg + "</p>")
if has_duplicate_ue_codes: if ues_with_duplicated_code:
H.append( H.append(
"""<div class="ue_warning"><span>Attention: plusieurs UE de cette f"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
formation ont le même code. Il faut corriger cela ci-dessous, formation ont le même code : <tt>{
sinon les calculs d'ECTS seront erronés !</span></div>""" ', '.join([
'<a class="stdlink" href="' + url_for( "notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"] )
+ '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>"
for ue in ues_with_duplicated_code ])
}</tt>.
Il faut corriger cela, sinon les capitalisations et ECTS seront
erronés !</span></div>"""
) )
# Description de la formation # Description de la formation
@ -699,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>
@ -735,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
) )
) )
else: else:
H.append('<div class="formation_classic_infos">')
H.append( H.append(
_ue_table_ues( _ue_table_ues(
parcours, parcours,
@ -764,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</ul> </ul>
""" """
) )
H.append("</div>")
H.append("</div>") # formation_ue_list H.append("</div>") # formation_ue_list
if ues_externes: if ues_externes:
@ -913,10 +937,10 @@ def _ue_table_ues(
cur_ue_semestre_id = None cur_ue_semestre_id = None
iue = 0 iue = 0
for ue in ues: for ue in ues:
if ue["ects"]: if ue["ects"] is None:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
ue["ects_str"] = "" ue["ects_str"] = ""
else:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
if editable: if editable:
klass = "span_apo_edit" klass = "span_apo_edit"
else: else:
@ -930,8 +954,8 @@ def _ue_table_ues(
if cur_ue_semestre_id != ue["semestre_id"]: if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"] cur_ue_semestre_id = ue["semestre_id"]
if iue > 0: # if iue > 0:
H.append("</ul>") # H.append("</ul>")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:" lab = "Pas d'indication de semestre:"
else: else:
@ -953,7 +977,6 @@ def _ue_table_ues(
) )
else: else:
H.append(arrow_none) H.append(arrow_none)
iue += 1
ue["acro_titre"] = str(ue["acronyme"]) ue["acro_titre"] = str(ue["acronyme"])
if ue["titre"] != ue["acronyme"]: if ue["titre"] != ue["acronyme"]:
ue["acro_titre"] += " " + str(ue["titre"]) ue["acro_titre"] += " " + str(ue["titre"])
@ -1001,6 +1024,14 @@ def _ue_table_ues(
delete_disabled_icon, delete_disabled_icon,
) )
) )
if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
H.append(
f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
formation_id=ue['formation_id'], semestre_idx=ue['semestre_id'])
}">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>"""
)
iue += 1
return "\n".join(H) return "\n".join(H)
@ -1268,7 +1299,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)""" (chaque UE doit avoir un acronyme unique dans la formation)"""
) )
# On ne peut pas supprimer le code UE: # On ne peut pas supprimer le code UE:
if "ue_code" in args and not args["ue_code"]: if "ue_code" in args and not args["ue_code"]:
del args["ue_code"] del args["ue_code"]

View File

@ -53,7 +53,7 @@ from app.scodoc.sco_exceptions import ScoValueError
def apo_semset_maq_status( def apo_semset_maq_status(
semset_id="", semset_id: int,
allow_missing_apo=False, allow_missing_apo=False,
allow_missing_decisions=False, allow_missing_decisions=False,
allow_missing_csv=False, allow_missing_csv=False,
@ -65,7 +65,7 @@ def apo_semset_maq_status(
): ):
"""Page statut / tableau de bord""" """Page statut / tableau de bord"""
if not semset_id: if not semset_id:
raise ValueError("invalid null semset_id") raise ScoValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id) semset = sco_semset.SemSet(semset_id=semset_id)
semset.fill_formsemestres() semset.fill_formsemestres()
# autorise export meme si etudiants Apo manquants: # autorise export meme si etudiants Apo manquants:

View File

@ -405,7 +405,6 @@ def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre""" """Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = formsemestre.to_dict()
evals = nt.get_evaluations_etats() evals = nt.get_evaluations_etats()
nb_evals = len(evals) nb_evals = len(evals)
@ -416,8 +415,8 @@ def formsemestre_evaluations_cal(formsemestre_id):
today = time.strftime("%Y-%m-%d") today = time.strftime("%Y-%m-%d")
year = int(sem["annee_debut"]) year = formsemestre.date_debut.year
if sem["mois_debut_ord"] < 8: if formsemestre.date_debut.month < 8:
year -= 1 # calendrier septembre a septembre year -= 1 # calendrier septembre a septembre
events = {} # (day, halfday) : event events = {} # (day, halfday) : event
for e in evals: for e in evals:
@ -537,11 +536,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
"""Experimental: un tableau indiquant pour chaque évaluation """Experimental: un tableau indiquant pour chaque évaluation
le nombre de jours avant la publication des notes. le nombre de jours avant la publication des notes.
N'indique pas les évaluations de ratrapage ni celles des modules de bonus/malus. N'indique pas les évaluations de rattrapage ni celles des modules de bonus/malus.
""" """
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = formsemestre.to_dict()
evals = nt.get_evaluations_etats() evals = nt.get_evaluations_etats()
T = [] T = []
@ -607,7 +605,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
origin="Généré par %s le " % sco_version.SCONAME origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr() + scu.timedate_human_repr()
+ "", + "",
filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]), filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
) )
return tab.make_page(format=format) return tab.make_page(format=format)
@ -635,16 +633,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>' '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
% moduleimpl_id % moduleimpl_id
) )
mod_descr = ( mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % (
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' moduleimpl_id,
% ( Mod["code"] or "",
moduleimpl_id, Mod["titre"] or "?",
Mod["code"] or "", nomcomplet,
Mod["titre"] or "?", resp,
nomcomplet, link,
resp,
link,
)
) )
etit = E["description"] or "" etit = E["description"] or ""

View File

@ -39,6 +39,7 @@ 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 sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -221,7 +222,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip: if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase expnom = expnom.upper() # les noms dans la BD sont en uppercase
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~") try:
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
except ScoException:
etuds = []
else: else:
code_nip = code_nip or expnom code_nip = code_nip or expnom
if code_nip: if code_nip:

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

@ -595,11 +595,12 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
"""Description du semestre sous forme de table exportable """Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients Liste des modules et de leurs coefficients
""" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[
0
]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
Mlist = sco_moduleimpl.moduleimpl_withmodule_list( Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id, sort_by_ue=True formsemestre_id=formsemestre_id, sort_by_ue=True
@ -709,7 +710,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
titles["coefficient"] = "Coef. éval." titles["coefficient"] = "Coef. éval."
titles["evalcomplete_str"] = "Complète" titles["evalcomplete_str"] = "Complète"
titles["publish_incomplete_str"] = "Toujours Utilisée" titles["publish_incomplete_str"] = "Toujours Utilisée"
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), sem["titremois"]) title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois())
return GenTable( return GenTable(
columns_ids=columns_ids, columns_ids=columns_ids,
@ -986,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
@ -1077,7 +1077,7 @@ def formsemestre_status(formsemestre_id=None):
"</p>", "</p>",
] ]
if use_ue_coefs: if use_ue_coefs and not formsemestre.formation.is_apc():
H.append( H.append(
""" """
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p> <p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>

View File

@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table(
else: else:
H.append('<td colspan="%d"><em>en cours</em></td>') H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit: # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
ues = nt.get_ues_stat_dict(filter_sport=True) ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [ ues = [
ue ue
for ue in ues for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"]) if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
] ]
for ue in ues: for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"] code = decisions_ue[ue["ue_id"]]["code"]
else: else:
code = "" code = ""
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) ue_status = etud_ue_status[ue["ue_id"]]
moy_ue = ue_status["moy"] if ue_status else "" moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings explanation_ue = [] # list of strings
if code == ADM: if code == ADM:
@ -1250,7 +1252,7 @@ def check_formation_ues(formation_id):
for ue in ues: for ue in ues:
# formsemestres utilisant cette ue ? # formsemestres utilisant cette ue ?
sems = ndb.SimpleDictFetch( sems = ndb.SimpleDictFetch(
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.* """SELECT DISTINCT sem.id AS formsemestre_id, sem.*
FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi
WHERE sem.formation_id = %(formation_id)s WHERE sem.formation_id = %(formation_id)s
AND mod.id = mi.module_id AND mod.id = mi.module_id
@ -1269,11 +1271,11 @@ def check_formation_ues(formation_id):
return "", {} return "", {}
# Genere message HTML: # Genere message HTML:
H = [ H = [
"""<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation """<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
sont utilisées dans des sont utilisées dans des
semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour
la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation: la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
soit modifier le programme de la formation (définir des UE dans chaque semestre), soit modifier le programme de la formation (définir des UE dans chaque semestre),
soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
UE extérieure. UE extérieure.
<ul> <ul>
@ -1286,7 +1288,11 @@ def check_formation_ues(formation_id):
for x in ue_multiples[ue["ue_id"]] for x in ue_multiples[ue["ue_id"]]
] ]
slist = ", ".join( slist = ", ".join(
["%(titreannee)s (<em>semestre %(semestre_id)s</em>)" % s for s in sems] [
"""%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)"""
% s
for s in sems
]
) )
H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist)) H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
H.append("</ul></div>") H.append("</ul></div>")

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

@ -151,6 +151,8 @@ class Logo:
Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
""" """
self.logoname = secure_filename(logoname) self.logoname = secure_filename(logoname)
if not self.logoname:
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
self.scodoc_dept_id = dept_id self.scodoc_dept_id = dept_id
self.prefix = prefix or "" self.prefix = prefix or ""
if self.scodoc_dept_id: if self.scodoc_dept_id:
@ -276,7 +278,7 @@ class Logo:
if self.mm is None: if self.mm is None:
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">' return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
else: else:
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">' return f'<logo name="{self.logoname}" width="{self.mm[0]}mm">'
def last_modified(self): def last_modified(self):
path = Path(self.filepath) path = Path(self.filepath)

View File

@ -305,7 +305,10 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
if can_change: if can_change:
c_link = ( c_link = (
'<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>' '<a class="discretelink" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">%s</a>'
% (mod["moduleimpl_id"], mod["descri"]) % (
mod["moduleimpl_id"],
mod["descri"] or "<i>(inscrire des étudiants)</i>",
)
) )
else: else:
c_link = mod["descri"] c_link = mod["descri"]

View File

@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object):
# pour le DUT, le dernier est toujours S4. # pour le DUT, le dernier est toujours S4.
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1 # Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
# (licences et autres formations en 1 seule session)) # (licences et autres formations en 1 seule session))
self.semestre_non_terminal = ( self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
self.sem["semestre_id"] != self.parcours.NB_SEM
) # True | False
if self.sem["semestre_id"] == NO_SEMESTRE_ID: if self.sem["semestre_id"] == NO_SEMESTRE_ID:
self.semestre_non_terminal = False self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant: # Liste des semestres du parcours de cet étudiant:

View File

@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate):
PageTemplate.__init__(self, "ScolarsPageTemplate", [content]) PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None self.logo = None
logo = find_logo( logo = find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None logoname="bul_pdf_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=""
) )
if logo is None: if logo is None:
# Also try to use PV background # Also try to use PV background
logo = find_logo( logo = find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None logoname="letter_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=""
) )
if logo is not None: if logo is not None:
self.background_image_filename = logo.filepath self.background_image_filename = logo.filepath

View File

@ -1296,11 +1296,21 @@ class BasePreferences(object):
"labels": ["non", "oui"], "labels": ["non", "oui"],
}, },
), ),
(
"bul_show_ue_coef",
{
"initvalue": 1,
"title": "Afficher coefficient des UE sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],
},
),
( (
"bul_show_coef", "bul_show_coef",
{ {
"initvalue": 1, "initvalue": 1,
"title": "Afficher coefficient des ue/modules sur les bulletins", "title": "Afficher coefficient des modules sur les bulletins",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"category": "bul", "category": "bul",
"labels": ["non", "oui"], "labels": ["non", "oui"],

View File

@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate):
background = find_logo( background = find_logo(
logoname="pvjury_background", logoname="pvjury_background",
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="", prefix="",
) )
else: else:
background = find_logo( background = find_logo(
logoname="letter_background", logoname="letter_background",
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="", prefix="",
) )
if not self.background_image_filename and background is not None: if not self.background_image_filename and background is not None:

View File

@ -854,23 +854,27 @@ def formsemestre_import_etud_admission(
apo_emailperso = etud.get("mailperso", "") apo_emailperso = etud.get("mailperso", "")
if info["emailperso"] and not apo_emailperso: if info["emailperso"] and not apo_emailperso:
apo_emailperso = info["emailperso"] apo_emailperso = info["emailperso"]
if ( if import_email:
import_email if not "mail" in etud:
and info["email"] != etud["mail"] raise ScoValueError(
or info["emailperso"] != apo_emailperso "la réponse portail n'a pas le champs requis 'mail'"
): )
sco_etud.adresse_edit( if (
cnx, info["email"] != etud["mail"]
args={ or info["emailperso"] != apo_emailperso
"etudid": etudid, ):
"adresse_id": info["adresse_id"], sco_etud.adresse_edit(
"email": etud["mail"], cnx,
"emailperso": apo_emailperso, args={
}, "etudid": etudid,
) "adresse_id": info["adresse_id"],
# notifie seulement les changements d'adresse mail institutionnelle "email": etud["mail"],
if info["email"] != etud["mail"]: "emailperso": apo_emailperso,
changed_mails.append((info, etud["mail"])) },
)
# notifie seulement les changements d'adresse mail institutionnelle
if info["email"] != etud["mail"]:
changed_mails.append((info, etud["mail"]))
else: else:
unknowns.append(code_nip) unknowns.append(code_nip)
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"]) sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])

View File

@ -50,7 +50,7 @@ import pydot
import requests import requests
from flask import g, request from flask import g, request
from flask import url_for, make_response, jsonify from flask import flash, url_for, make_response, jsonify
from config import Config from config import Config
from app import log from app import log
@ -616,6 +616,16 @@ def bul_filename(sem, etud, format):
return filename return filename
def flash_errors(form):
"""Flashes form errors (version sommaire)"""
for field, errors in form.errors.items():
flash(
"Erreur: voir le champs %s" % (getattr(form, field).label.text,),
"warning",
)
# see https://getbootstrap.com/docs/4.0/components/alerts/
def sendCSVFile(data, filename): # DEPRECATED utiliser send_file def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV.""" """publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True) return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)
@ -635,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(
@ -659,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
@ -669,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)
@ -789,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

@ -138,7 +138,7 @@ div.head_message {
border-radius: 8px; border-radius: 8px;
font-family : arial, verdana, sans-serif ; font-family : arial, verdana, sans-serif ;
font-weight: bold; font-weight: bold;
width: 40%; width: 70%;
text-align: center; text-align: center;
} }
@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */ width: 75px; /* la marge fait 130px */
} }
div.logo-logo { div.logo-logo {
margin-left: -5px;
text-align: center ; text-align: center ;
} }
div.logo-logo img { div.logo-logo img {
box-sizing: content-box; box-sizing: content-box;
margin-top: -10px; margin-top: 10px; /* -10px */
width: 128px; width: 135px; /* 128px */
padding-right: 5px; padding-right: 5px;
margin-left: -75px;
} }
div.sidebar-bottom { div.sidebar-bottom {
margin-top: 10px; margin-top: 10px;
@ -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;
} }
@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list {
padding-top: 5px; padding-top: 5px;
padding-bottom: 5px; padding-bottom: 5px;
} }
span.missing_ue_ects {
color: red;
font-weight: bold;
}
li.module_malus span.formation_module_tit { li.module_malus span.formation_module_tit {
color: red; color: red;
font-weight: bold; font-weight: bold;
@ -1703,10 +1706,16 @@ ul.notes_ue_list {
padding-bottom: 1em; padding-bottom: 1em;
font-weight: bold; font-weight: bold;
} }
.formation_classic_infos ul.notes_ue_list {
padding-top: 0px;
}
li.notes_ue_list { .formation_classic_infos li.notes_ue_list {
margin-top: 9px; margin-top: 9px;
list-style-type: none; list-style-type: none;
border: 1px solid maroon;
border-radius: 10px;
padding-bottom: 5px;
} }
span.ue_type_1 { span.ue_type_1 {
color: green; color: green;
@ -1749,6 +1758,7 @@ ul.notes_matiere_list {
background-color: rgb(220,220,220); background-color: rgb(220,220,220);
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
border-top: 1px solid maroon;
} }
ul.notes_module_list { ul.notes_module_list {
@ -1757,6 +1767,11 @@ ul.notes_module_list {
font-style: normal; font-style: normal;
} }
div.ue_list_tit_sem {
font-size: 120%;
font-weight: bold;
}
.notes_ue_list a.stdlink { .notes_ue_list a.stdlink {
color: #001084; color: #001084;
text-decoration: underline; text-decoration: underline;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 37 KiB

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

@ -91,7 +91,7 @@ class releveBUT extends HTMLElement {
<div> <div>
<div class=decision></div> <div class=decision></div>
<div class=dateInscription>Inscrit le </div> <div class=dateInscription>Inscrit le </div>
<em>Les moyennes servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em> <em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
</div> </div>
</div> </div>

View File

@ -61,12 +61,10 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% for category, message in messages %}
{% for message in messages %} <div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
<div class="alert alert-info" role="alert">{{ message }}</div> {% endfor %}
{% endfor %}
{% endif %}
{% endwith %} {% endwith %}
{# application content needs to be provided in the app_content block #} {# application content needs to be provided in the app_content block #}

View File

@ -18,10 +18,12 @@
<a href="{{ url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept, ) }}"> <a href="{{ url_for('notes.refcomp_table', scodoc_dept=g.scodoc_dept, ) }}">
Liste des référentiels de compétences chargés</a> Liste des référentiels de compétences chargés</a>
</li> </li>
{% if formation is not none %}
<li> <li>
<a href="{{ url_for('notes.refcomp_assoc_formation', scodoc_dept=g.scodoc_dept, formation_id=formation.id) }}"> <a href="{{ url_for('notes.refcomp_assoc_formation', scodoc_dept=g.scodoc_dept, formation_id=formation.id) }}">
Association à la formation {{ formation.acronyme }}</a> Association à la formation {{ formation.acronyme }}</a>
</li> </li>
{% endif %}
</div> </div>
</div> </div>

View File

@ -0,0 +1,9 @@
{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #}
{# -*- mode: jinja-html -*- #}
<div class="head_message_container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="head_message alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
</div>

View File

@ -65,6 +65,13 @@
{% endfor %} {% endfor %}
</span> </span>
{% if mod.ue.type != 0 and mod.module_type != 0 %}
<span class="warning" title="Une UE de type spécial ne
devrait contenir que des modules standards">
type incompatible avec son UE de rattachement !
</span>
{% endif %}
<span class="sco_tag_edit"><form><textarea data-module_id="{{mod.id}}" <span class="sco_tag_edit"><form><textarea data-module_id="{{mod.id}}"
class="{% if tag_editable %}module_tag_editor{% else %}module_tag_editor_ro{% endif %}">{{mod.tags|join(', ', attribute='title')}}</textarea></form></span> class="{% if tag_editable %}module_tag_editor{% else %}module_tag_editor_ro{% endif %}">{{mod.tags|join(', ', attribute='title')}}</textarea></form></span>

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',
@ -38,7 +36,8 @@
{% set virg = joiner(", ") %} {% set virg = joiner(", ") %}
<span class="ue_code">( <span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%} {%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
{{ virg() }}{{ue.ects or 0}} ECTS) {{ virg() }}{{ue.ects if ue.ects is not none
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS)
</span> </span>
</span> </span>
@ -48,6 +47,9 @@
}}">modifier</a> }}">modifier</a>
{% endif %} {% endif %}
{% if ue.type == 1 and ue.modules.count() == 0 %}
<span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -23,12 +23,10 @@
<div id="gtrcontent" class="gtrcontent"> <div id="gtrcontent" class="gtrcontent">
<div class="container"> <div class="container">
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% for category, message in messages %}
{% for message in messages %} <div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
<div class="alert alert-info" role="alert">{{ message }}</div> {% endfor %}
{% endfor %}
{% endif %}
{% endwith %} {% endwith %}
</div> </div>
{% if sco.sem %} {% if sco.sem %}

View File

@ -4,7 +4,7 @@
<div class="sidebar"> <div class="sidebar">
{# sidebar_common #} {# sidebar_common #}
<a class="scodoc_title" href="{{ <a class="scodoc_title" href="{{
url_for('scodoc.index', scodoc_dept=g.scodoc_dept) }}">ScoDoc 9.2a</a> url_for('scodoc.index', scodoc_dept=g.scodoc_dept) }}">ScoDoc {{ sco.SCOVERSION }}</a>
<div id="authuser"><a id="authuserlink" href="{{ <div id="authuser"><a id="authuserlink" href="{{
url_for('users.user_info_page', scodoc_dept=g.scodoc_dept, user_name=current_user.user_name) url_for('users.user_info_page', scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
}}">{{current_user.user_name}}</a> }}">{{current_user.user_name}}</a>

View File

@ -16,6 +16,7 @@ from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
import sco_version
scodoc_bp = Blueprint("scodoc", __name__) scodoc_bp = Blueprint("scodoc", __name__)
scolar_bp = Blueprint("scolar", __name__) scolar_bp = Blueprint("scolar", __name__)
@ -53,6 +54,7 @@ class ScoData:
# Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête) # Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête)
self.Permission = Permission self.Permission = Permission
self.scu = scu self.scu = scu
self.SCOVERSION = sco_version.SCOVERSION
# -- Informations étudiant courant, si sélectionné: # -- Informations étudiant courant, si sélectionné:
etudid = g.get("etudid", None) etudid = g.get("etudid", None)
if not etudid: if not etudid:

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

@ -35,7 +35,7 @@ from operator import itemgetter
from xml.etree import ElementTree from xml.etree import ElementTree
import flask import flask
from flask import flash, jsonify, render_template, url_for from flask import abort, flash, jsonify, render_template, url_for
from flask import current_app, g, request from flask import current_app, g, request
from flask_login import current_user from flask_login import current_user
from werkzeug.utils import redirect from werkzeug.utils import redirect
@ -68,10 +68,14 @@ from app.scodoc import sco_utils as scu
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType from app.scodoc.sco_exceptions import (
AccessDenied,
ScoException,
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.pe import pe_view from app.pe import pe_view
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -2672,12 +2676,15 @@ def check_integrity_all():
def moduleimpl_list( def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json" moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
): ):
data = sco_moduleimpl.moduleimpl_list( try:
moduleimpl_id=moduleimpl_id, data = sco_moduleimpl.moduleimpl_list(
formsemestre_id=formsemestre_id, moduleimpl_id=moduleimpl_id,
module_id=module_id, formsemestre_id=formsemestre_id,
) module_id=module_id,
return scu.sendResult(data, format=format) )
return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom @bp.route("/do_moduleimpl_withmodule_list") # ancien nom
@ -2686,7 +2693,7 @@ def moduleimpl_list(
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def moduleimpl_withmodule_list( def moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
): ):
"""API ScoDoc 7""" """API ScoDoc 7"""
data = sco_moduleimpl.moduleimpl_withmodule_list( data = sco_moduleimpl.moduleimpl_withmodule_list(

View File

@ -304,8 +304,9 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
# stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici # stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
# from app.scodoc.sco_photos import _http_jpeg_file # from app.scodoc.sco_photos import _http_jpeg_file
logo = sco_logos.find_logo(name, dept_id, strict).select() logo = sco_logos.find_logo(name, dept_id, strict)
if logo is not None: if logo is not None:
logo.select()
suffix = logo.suffix suffix = logo.suffix
if small: if small:
with PILImage.open(logo.filepath) as im: with PILImage.open(logo.filepath) as im:

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

@ -19,31 +19,6 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.add_column("apc_competence", sa.Column("id_orebut", sa.Text(), nullable=True))
op.drop_constraint(
"apc_competence_referentiel_id_titre_key", "apc_competence", type_="unique"
)
op.create_index(
op.f("ix_apc_competence_id_orebut"),
"apc_competence",
["id_orebut"],
)
op.add_column(
"apc_referentiel_competences", sa.Column("annexe", sa.Text(), nullable=True)
)
op.add_column(
"apc_referentiel_competences",
sa.Column("type_structure", sa.Text(), nullable=True),
)
op.add_column(
"apc_referentiel_competences",
sa.Column("type_departement", sa.Text(), nullable=True),
)
op.add_column(
"apc_referentiel_competences",
sa.Column("version_orebut", sa.Text(), nullable=True),
)
op.create_index( op.create_index(
op.f("ix_notes_formsemestre_uecoef_formsemestre_id"), op.f("ix_notes_formsemestre_uecoef_formsemestre_id"),
"notes_formsemestre_uecoef", "notes_formsemestre_uecoef",
@ -80,15 +55,10 @@ def downgrade():
table_name="notes_formsemestre_uecoef", table_name="notes_formsemestre_uecoef",
) )
op.drop_column("apc_referentiel_competences", "version_orebut")
op.drop_column("apc_referentiel_competences", "type_departement")
op.drop_column("apc_referentiel_competences", "type_structure")
op.drop_column("apc_referentiel_competences", "annexe")
op.drop_index(op.f("ix_apc_competence_id_orebut"), table_name="apc_competence") op.drop_index(op.f("ix_apc_competence_id_orebut"), table_name="apc_competence")
op.create_unique_constraint( op.create_unique_constraint(
"apc_competence_referentiel_id_titre_key", "apc_competence_referentiel_id_titre_key",
"apc_competence", "apc_competence",
["referentiel_id", "titre"], ["referentiel_id", "titre"],
) )
op.drop_column("apc_competence", "id_orebut")
# ### end Alembic commands ### # ### end Alembic commands ###

View File

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

View File

@ -93,6 +93,8 @@ fi
# nginx: # nginx:
mkdir -p "$slash"/etc/nginx/sites-available || die "can't mkdir nginx config" mkdir -p "$slash"/etc/nginx/sites-available || die "can't mkdir nginx config"
cp -p "$SCODOC_DIR"/tools/etc/scodoc9.nginx "$slash"/etc/nginx/sites-available/scodoc9.nginx.distrib || die "can't copy nginx config" cp -p "$SCODOC_DIR"/tools/etc/scodoc9.nginx "$slash"/etc/nginx/sites-available/scodoc9.nginx.distrib || die "can't copy nginx config"
mkdir -p "$slash"/etc/nginx/conf.d || die "can't mkdir nginx conf.d"
cp -p "$SCODOC_DIR"/tools/etc/scodoc9-nginx-timeout.conf "$slash"/etc/nginx/conf.d/ || die "can't copy nginx timeout config"
# systemd # systemd
mkdir -p "$slash"/etc/systemd/system/ || die "can't mkdir systemd config" mkdir -p "$slash"/etc/systemd/system/ || die "can't mkdir systemd config"

View File

@ -0,0 +1,5 @@
# Reglage des timeout du frontal nginx pour ScoDoc 9 (>= 9.1.59)
proxy_read_timeout 400;
proxy_connect_timeout 400;
proxy_send_timeout 400;