diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index 098bd128..b2721941 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -21,6 +21,7 @@ from flask import g
from app.models.formsemestre import FormSemestre
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
@@ -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).
"""
- # 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
# Attributs virtuels:
@@ -198,23 +199,29 @@ class BonusSportAdditif(BonusSport):
à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
- seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
+ seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte
+ seuil_comptage = (
+ None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen)
+ )
proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus
sem_modimpl_moys_inscrits: les notes de sport
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:
# pas d'étudiants ou pas d'UE ou pas de module...
return
+ seuil_comptage = (
+ self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
+ )
bonus_moy_arr = np.sum(
np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
- (sem_modimpl_moys_inscrits - self.seuil_moy_gen)
- * self.proportion_point,
+ (sem_modimpl_moys_inscrits - seuil_comptage) * self.proportion_point,
0.0,
),
axis=1,
@@ -227,13 +234,27 @@ class BonusSportAdditif(BonusSport):
else: # necessaire pour éviter bonus négatifs !
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)
- 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
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
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:
# Bonus sur la moyenne générale seulement
self.bonus_moy_gen = pd.Series(
@@ -284,6 +305,7 @@ class BonusSportMultiplicatif(BonusSport):
class BonusDirect(BonusSportAdditif):
"""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.
(rappel: la note est ramenée sur 20 avant application).
"""
@@ -294,47 +316,108 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0
-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 :
- 0.05 point si >=10,
- 0.1 point si >=12,
- 0.15 point si >=14,
- 0.2 point si >=16,
- 0.25 point si >=18.
+class BonusAisneStQuentin(BonusSportAdditif):
+ """Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin
+
+
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+ de l'Université de St Quentin non rattachés à une unité d'enseignement.
+
+
+ - Si la note est >= 10 et < 12.1, bonus de 0.1 point
+ - Si la note est >= 12.1 et < 14.1, bonus de 0.2 point
+ - Si la note est >= 14.1 et < 16.1, bonus de 0.3 point
+ - Si la note est >= 16.1 et < 18.1, bonus de 0.4 point
+ - Si la note est >= 18.1, bonus de 0.5 point
+
+
+ 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).
+
"""
- name = "bonus_iut_annecy"
- displayed_name = "IUT d'Annecy"
+ name = "bonus_iutstq"
+ displayed_name = "IUT de Saint-Quentin"
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 >= 18.0] = 0.25
- 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
+ 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:
+ with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
+ bonus_moy_arr = np.sum(
+ sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
+ ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
+ np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
- # Bonus 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,
- )
+ bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
+ bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
+ bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
+ bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
+ bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
+ bonus_moy_arr[bonus_moy_arr >= 10] = 0.1
+
+ self.bonus_additif(bonus_moy_arr)
+
+
+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 :
+# 0.05 point si >=10,
+# 0.1 point si >=12,
+# 0.15 point si >=14,
+# 0.2 point si >=16,
+# 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):
@@ -373,26 +456,150 @@ class BonusBezier(BonusSportAdditif):
class BonusBordeaux1(BonusSportMultiplicatif):
- """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale
- et UE.
-
+ """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1,
+ sur moyenne générale et UEs.
+
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.
-
+
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.
- Formule : le % = points>moyenne / 2
+ qui augmente la moyenne de chaque UE et la moyenne générale.
+ Formule : pourcentage = (points au dessus de 10) / 2
+
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
-
+
"""
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
seuil_moy_gen = 10.0
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.
+
+ Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+ de l'Université (sport, théâtre) non rattachés à une unité d'enseignement.
+
+ 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.
+ Formule : pourcentage = (points au dessus de 10) / 2
+
+ Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
+
+ """
+
+ 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.
+
+
+ - DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
+ bonifie les moyennes d'UE (sauf l'UE41 dont le code est UE41_E) à raison
+ de bonus = (option - 10)/10.
+
+
+ - BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
+ les moyennes d'UE à raison de bonus = (option - 10)*5%.
+
+ """
+
+ 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 :
+
+ - en DUT à la moyenne générale du semestre déjà obtenue par l'étudiant.
+
- en BUT et LP à la moyenne des UE dont l'acronyme fini par BS (ex : UE2.1BS, UE32BS)
+
+ """
+
+ 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):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Colmar.
@@ -417,19 +624,21 @@ class BonusColmar(BonusSportAdditif):
class BonusGrenobleIUT1(BonusSportMultiplicatif):
"""Bonus IUT1 de Grenoble
+
À compter de sept. 2021:
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
la formule : bonification (en %) = (note-10)*0,5.
-
- Bonification qui ne s'applique que si la note est >10.
-
- (Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif)
-
+
+ La bonification ne s'applique que si la note est supérieure à 10.
+
+ (Une note de 10 donne donc 0% de bonif, et une note de 20 : 5% de bonif)
+
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
Chaque point correspondait à 0.25% d'augmentation de la moyenne
générale.
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
+
"""
name = "bonus_iut1grenoble_2017"
@@ -456,15 +665,18 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
class BonusLaRochelle(BonusSportAdditif):
"""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.
- 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).
+
+ - Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.
+ - 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).
+
"""
name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle"
- seuil_moy_gen = 10.0 # tous les points sont comptés
- proportion_point = 0.01
+ seuil_moy_gen = 10.0 # si bonus > 10,
+ seuil_comptage = 0.0 # tous les points sont comptés
+ proportion_point = 0.01 # 1%
class BonusLeHavre(BonusSportMultiplicatif):
@@ -483,16 +695,17 @@ class BonusLeHavre(BonusSportMultiplicatif):
class BonusLeMans(BonusSportAdditif):
"""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
+ Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés.
+
+
+ - En BUT: la moyenne de chacune des UE du semestre est augmentée de
+ 2% du cumul des points de bonus;
-
- En BUT: la moyenne de chacune des UE du semestre est augmentée de
- 2% du cumul des points de bonus,
-
- 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.
+ - 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"
@@ -516,12 +729,13 @@ class BonusLeMans(BonusSportAdditif):
class BonusLille(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq
- Les étudiants de l'IUT peuvent suivre des enseignements optionnels
+ Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Lille (sports, etc) 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 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.
+
"""
name = "bonus_lille"
@@ -573,17 +787,19 @@ class BonusMulhouse(BonusSportAdditif):
class BonusNantes(BonusSportAdditif):
"""IUT de Nantes (Septembre 2018)
- Nous avons différents types de bonification
+ Nous avons différents types de bonification
(sport, culture, engagement citoyen).
-
+
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
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.
-
- Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura 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
- valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
+
+ Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura
+ 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 valeur de la bonification: entrer 0,1/20 signifiera
+ un bonus de 0,1 point la moyenne générale).
+
"""
name = "bonus_nantes"
@@ -604,7 +820,8 @@ class BonusRoanne(BonusSportAdditif):
displayed_name = "IUT de Roanne"
seuil_moy_gen = 0.0
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):
@@ -627,13 +844,14 @@ class BonusStDenis(BonusSportAdditif):
class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours.
- Les notes des UE bonus (ramenées sur 20) sont sommées
+ 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,
soit pour le BUT à chaque moyenne d'UE.
-
- Attention: en GEII, facteur 1/40, ailleurs facteur 1.
-
+
+ Attention: en GEII, facteur 1/40, ailleurs facteur 1.
+
Le bonus total est limité à 1 point.
+
"""
name = "bonus_tours"
@@ -658,11 +876,13 @@ class BonusVilleAvray(BonusSport):
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
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
- Si la note est >= 12 et < 16, bonus de 0.2 point
- Si la note est >= 16, bonus de 0.3 point
- Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
- l'étudiant.
+
+ - Si la note est >= 10 et < 12, bonus de 0.1 point
+ - Si la note est >= 12 et < 16, bonus de 0.2 point
+ - Si la note est >= 16, bonus de 0.3 point
+
+ Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
+ l'étudiant.
"""
name = "bonus_iutva"
@@ -670,21 +890,21 @@ class BonusVilleAvray(BonusSport):
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""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:
- bonus_moy_arr = np.sum(
- sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
- ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
- bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
- bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
+ with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
+ bonus_moy_arr = np.sum(
+ sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
+ ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
+ np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
+ bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
+ bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
+ bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
- # Bonus moyenne générale, et 0 sur les UE
- 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.
+ self.bonus_additif(bonus_moy_arr)
class BonusIUTV(BonusSportAdditif):
@@ -700,7 +920,7 @@ class BonusIUTV(BonusSportAdditif):
name = "bonus_iutv"
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):
diff --git a/app/comp/moy_mat.py b/app/comp/moy_mat.py
new file mode 100644
index 00000000..0a752263
--- /dev/null
+++ b/app/comp/moy_mat.py
@@ -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
diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py
index f30f0491..eea357e8 100644
--- a/app/comp/moy_mod.py
+++ b/app/comp/moy_mod.py
@@ -335,15 +335,17 @@ class ModuleImplResultsAPC(ModuleImplResults):
notes_rat / (eval_rat.note_max / 20.0),
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
- etuds_use_rattrapage = notes_rat > etuds_moy_module
+ etuds_use_rattrapage = notes_rat_ues > etuds_moy_module
etuds_moy_module = np.where(
- etuds_use_rattrapage[:, np.newaxis],
- np.tile(notes_rat[:, np.newaxis], nb_ues),
- etuds_moy_module,
+ etuds_use_rattrapage, notes_rat_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(
- 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(
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
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
(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)
"""
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]
evaluation_ids = [evaluation.id for evaluation in evaluations]
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
- for ue_poids in EvaluationUEPoids.query.join(
- EvaluationUEPoids.evaluation
- ).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...
+ if (
+ modimpl.module.module_type == ModuleType.RESSOURCE
+ or modimpl.module.module_type == ModuleType.SAE
+ ):
+ for ue_poids in EvaluationUEPoids.query.join(
+ EvaluationUEPoids.evaluation
+ ).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:
default_poids = (
diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py
index 5caa3d39..db42616c 100644
--- a/app/comp/moy_sem.py
+++ b/app/comp/moy_sem.py
@@ -30,8 +30,10 @@
import numpy as np
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
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
@@ -48,6 +50,28 @@ def compute_sem_moys_apc(
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):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos.
diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py
index 84a7e2bf..efbe7cd3 100644
--- a/app/comp/moy_ue.py
+++ b/app/comp/moy_ue.py
@@ -27,7 +27,6 @@
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
"""
-from re import X
import numpy as np
import pandas as pd
@@ -218,21 +217,25 @@ def compute_ue_moys_apc(
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame,
+ modimpl_mask: np.array,
) -> pd.DataFrame:
"""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
NI non inscrit à (au moins un) module de cette UE
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
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
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)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
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
"""
@@ -249,7 +252,8 @@ def compute_ue_moys_apc(
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules
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:
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_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
- """Calcul de la moyenne d'UE en mode classique.
+ """Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
+
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
@@ -359,7 +364,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues
)
- # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
+ # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
@@ -404,6 +409,68 @@ def compute_ue_moys_classic(
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
+def compute_mat_moys_classic(
+ sem_matrix: np.array,
+ modimpl_inscr_df: pd.DataFrame,
+ modimpl_coefs: np.array,
+ modimpl_mask: np.array,
+) -> pd.Series:
+ """Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
+
+ La moyenne est un nombre (note/20 ou NaN.
+
+ Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
+ permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
+
+ sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
+ ndarray (etuds x modimpls)
+ (floats avec des NaN)
+ etuds : listes des étudiants (dim. 0 de la matrice)
+ modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
+ modimpl_coefs: vecteur des coefficients de modules
+ modimpl_mask: masque des modimpls à prendre en compte
+
+ Résultat:
+ - moyennes: pd.Series, index etudid
+ """
+ if (not len(modimpl_mask)) or (
+ sem_matrix.shape[0] == 0
+ ): # aucun module ou aucun étudiant
+ # etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
+ return pd.Series(
+ [0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
+ )
+ # Restreint aux modules sélectionnés:
+ sem_matrix = sem_matrix[:, modimpl_mask]
+ modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
+ modimpl_coefs = modimpl_coefs[modimpl_mask]
+
+ nb_etuds, nb_modules = sem_matrix.shape
+ assert len(modimpl_coefs) == nb_modules
+
+ # Enlève les NaN du numérateur:
+ sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
+ # Ne prend pas en compte les notes des étudiants non inscrits au module:
+ # Annule les notes:
+ sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
+ # Annule les coefs des modules où l'étudiant n'est pas inscrit:
+ modimpl_coefs_etuds = np.where(
+ modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
+ )
+ # Annule les coefs des modules NaN (nb_etuds x nb_mods)
+ modimpl_coefs_etuds_no_nan = np.where(
+ np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
+ )
+ if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
+ modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
+
+ etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
+ axis=1
+ ) / modimpl_coefs_etuds_no_nan.sum(axis=1)
+
+ return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
+
+
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
diff --git a/app/comp/res_but.py b/app/comp/res_but.py
index b74efb10..7bd79463 100644
--- a/app/comp/res_but.py
+++ b/app/comp/res_but.py
@@ -14,7 +14,7 @@ from app import log
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
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.scodoc.sco_codes_parcours import UE_SPORT
@@ -56,14 +56,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
# modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
- # Elimine les coefs des modimpl bonus sports:
- modimpls_sport = [
- modimpl
+ # Masque de tous les modules _sauf_ les bonus (sport)
+ modimpls_mask = [
+ modimpl.module.ue.type != UE_SPORT
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.sem_cube,
@@ -72,10 +69,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
+ modimpls_mask,
)
# Les coefficients d'UE ne sont pas utilisés en APC
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
@@ -85,7 +83,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue -= self.malus
# --- 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()
if bonus_class is not None:
bonus: BonusSport = bonus_class(
@@ -100,13 +98,20 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None:
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:
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative)
- self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
- self.etud_moy_ue, self.modimpl_coefs_df
+ # self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
+ # 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
self.apply_capitalisation()
diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py
index 2caf515c..b36eaaf6 100644
--- a/app/comp/res_classic.py
+++ b/app/comp/res_classic.py
@@ -15,7 +15,7 @@ from flask import g, url_for
from app import db
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.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
@@ -24,6 +24,7 @@ from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
+from app.scodoc import sco_preferences
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_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 = {
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.clip(lower=0.0, upper=20.0, inplace=True)
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.clip(lower=0.0, upper=20.0, inplace=True)
- # compat nt, utilisé pour l'afficher sur les bulletins:
- self.bonus = bonus_mg
+ elif bonus_mg is not None:
+ # Applique le bonus moyenne générale renvoyé
+ self.etud_moy_gen += bonus_mg
+
+ # compat nt, utilisé pour l'afficher sur les bulletins:
+ self.bonus = bonus_mg
+
# --- UE capitalisées
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:
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:
"""La moyenne de l'étudiant dans le moduleimpl
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:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index b019c977..8fa106f5 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np
import pandas as pd
+from flask import g, flash, url_for
+
from app import log
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
-from app.models import FormSemestre, Identite, ModuleImpl
-from app.models import FormSemestreUECoef
+from app.models import FormSemestre, FormSemestreUECoef
+from app.models import Identite
+from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
+from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@@ -39,6 +43,7 @@ class ResultatsSemestre(ResultatsCache):
"modimpl_inscr_df",
"modimpls_results",
"etud_coef_ue_df",
+ "moyennes_matieres",
)
def __init__(self, formsemestre: FormSemestre):
@@ -57,6 +62,8 @@ class ResultatsSemestre(ResultatsCache):
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None
+ self.moyennes_matieres = {}
+ """Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
def compute(self):
"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,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
- # return # XXX XXX XXX
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees
@@ -184,10 +190,12 @@ class ResultatsSemestre(ResultatsCache):
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
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
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_coefs_ue += coef
@@ -195,13 +203,25 @@ class ResultatsSemestre(ResultatsCache):
# On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale
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]
if isinstance(capitalisations, pd.DataFrame):
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
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
@@ -209,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
- ue_cap = None
- return ue_cap
+ return None
+ # 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:
"""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]
moy_ue = cur_moy_ue
is_capitalized = False # si l'UE prise en compte est une UE capitalisée
- was_capitalized = (
- False # s'il y a precedemment une UE capitalisée (pas forcement meilleure)
- )
+ # s'il y a precedemment une UE capitalisée (pas forcement meilleure):
+ was_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
- if (
- ue_cap is not None
- and not ue_cap.empty
- and not np.isnan(ue_cap["moy_ue"])
- ):
+ if ue_cap and not np.isnan(ue_cap["moy_ue"]):
was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"]
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
+ via cette page.
+ """
+ )
+ 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 {
"is_capitalized": is_capitalized,
@@ -375,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre):
"""Stats (moy/min/max) sur la moyenne générale"""
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.
Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE }
"""
- ues = []
- for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
+ ues = self.formsemestre.query_ues(with_sport=not filter_sport)
+ ues_dict = []
+ for ue in ues:
d = ue.to_dict()
if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id]
else:
moys = None
d.update(StatsMoyenne(moys).to_dict())
- ues.append(d)
- return ues
+ ues_dict.append(d)
+ 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]:
"""Liste des modules pour une UE (ou toutes si ue_id==None),
@@ -508,10 +562,15 @@ class NotesTableCompat(ResultatsSemestre):
return ""
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)"""
- # non supporté en 9.2
- return "na"
+ if not self.moyennes_matieres:
+ 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:
"""La moyenne de l'étudiant dans le moduleimpl
diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py
index 607ad168..e27a157c 100644
--- a/app/comp/res_sem.py
+++ b/app/comp/res_sem.py
@@ -8,11 +8,13 @@
"""
from flask import g
+from app import db
from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre
+from app.scodoc import sco_cache
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
@@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it.
"""
+ is_apc = formsemestre.formation.is_apc()
+ if is_apc and formsemestre.semestre_id == -1:
+ formsemestre.semestre_id = 1
+ db.session.add(formsemestre)
+ db.session.commit()
+ sco_cache.invalidate_formsemestre(formsemestre.id)
+
# --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_results_cache = {}
@@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id]
- klass = (
- ResultatsSemestreBUT
- if formsemestre.formation.is_apc()
- else ResultatsSemestreClassic
- )
+ klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id]
diff --git a/app/decorators.py b/app/decorators.py
index 8ebf5dea..220ece56 100644
--- a/app/decorators.py
+++ b/app/decorators.py
@@ -193,7 +193,7 @@ def scodoc7func(func):
# necessary for db ids and boolean values
try:
v = int(v)
- except ValueError:
+ except (ValueError, TypeError):
pass
pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py
index 2be78713..c8998327 100644
--- a/app/forms/main/config_logos.py
+++ b/app/forms/main/config_logos.py
@@ -30,17 +30,15 @@ Formulaires configuration logos
Contrib @jmp, dec 21
"""
-import re
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
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 app import AccessDenied
from app.models import Departement
-from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
@@ -49,10 +47,11 @@ from app.scodoc.sco_config_actions import (
LogoInsert,
)
-from flask_login import current_user
+from app.scodoc import sco_utils as scu
from app.scodoc.sco_logos import find_logo
+
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@@ -111,6 +110,15 @@ def dept_key_to_id(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):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
@@ -118,11 +126,7 @@ class AddLogoForm(FlaskForm):
name = StringField(
label="Nom",
validators=[
- validators.regexp(
- r"^[a-zA-Z0-9-_]*$",
- re.IGNORECASE,
- "Ne doit comporter que lettres, chiffres, _ ou -",
- ),
+ logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
@@ -373,11 +377,11 @@ def config_logos():
if action:
action.execute()
flash(action.message)
- return redirect(
- url_for(
- "scodoc.configure_logos",
- )
- )
+ return redirect(url_for("scodoc.configure_logos"))
+ else:
+ if not form.validate():
+ scu.flash_errors(form)
+
return render_template(
"config_logos.html",
scodoc_dept=None,
diff --git a/app/models/departements.py b/app/models/departements.py
index ebe5cc14..44a963ca 100644
--- a/app/models/departements.py
+++ b/app/models/departements.py
@@ -55,6 +55,9 @@ def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement"
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)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1)
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 24514248..41ede854 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -104,6 +104,11 @@ class FormSemestre(db.Model):
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
# ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True)
@@ -201,7 +206,11 @@ class FormSemestre(db.Model):
modimpls = self.modimpls.all()
if self.formation.is_apc():
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:
modimpls.sort(
diff --git a/app/models/groups.py b/app/models/groups.py
index 902298cc..976d465b 100644
--- a/app/models/groups.py
+++ b/app/models/groups.py
@@ -31,6 +31,11 @@ class Partition(db.Model):
show_in_lists = db.Column(
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):
super(Partition, self).__init__(**kwargs)
@@ -42,6 +47,9 @@ class Partition(db.Model):
else:
self.numero = 1
+ def __repr__(self):
+ return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">"""
+
class GroupDescr(db.Model):
"""Description d'un groupe d'une partition"""
@@ -55,6 +63,11 @@ class GroupDescr(db.Model):
# "A", "C2", ... (NULL for 'all'):
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",
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index 700dec26..0aa74ef4 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls
"""
import pandas as pd
+import flask_sqlalchemy
from app import db
from app.comp import df_cache
@@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
)
@classmethod
- def nb_inscriptions_dans_ue(
+ def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
- ) -> int:
- """Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
+ ) -> flask_sqlalchemy.BaseQuery:
+ """moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.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)
+ ]
+ )
diff --git a/app/models/ues.py b/app/models/ues.py
index 09469fb0..2bed88a3 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -54,13 +54,15 @@ class UniteEns(db.Model):
'EXTERNE' if self.is_external else ''})>"""
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.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["ue_id"] = self.id
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["code_apogee"] = e["code_apogee"] or "" # pas de None
return e
diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py
index 5a98d375..5af1a575 100644
--- a/app/pe/pe_view.py
+++ b/app/pe/pe_view.py
@@ -36,6 +36,7 @@
"""
from flask import send_file, request
+from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre
@@ -97,8 +98,12 @@ def pe_view_sem_recap(
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
- template_latex = avis_tmpl_file.read().decode('utf-8')
- template_latex = template_latex
+ try:
+ template_latex = avis_tmpl_file.read().decode("utf-8")
+ except UnicodeDecodeError as e:
+ raise ScoValueError(
+ "Données (template) invalides (caractères non UTF8 ?)"
+ ) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
@@ -114,7 +119,7 @@ def pe_view_sem_recap(
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
- footer_latex = footer_tmpl_file.read().decode('utf-8')
+ footer_latex = footer_tmpl_file.read().decode("utf-8")
footer_latex = footer_latex
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py
index 23a8b834..653cdb80 100644
--- a/app/scodoc/html_sco_header.py
+++ b/app/scodoc/html_sco_header.py
@@ -30,7 +30,7 @@
import html
-from flask import g
+from flask import render_template
from flask import request
from flask_login import current_user
@@ -280,6 +280,9 @@ def sco_header(
if not no_side_bar:
H.append(html_sidebar.sidebar())
H.append("""""")
+ # En attendant le replacement complet de cette fonction,
+ # inclusion ici des messages flask
+ H.append(render_template("flashed_messages.html"))
#
# Barre menu semestre:
H.append(formsemestre_page_title())
diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py
index e7710a91..b1482ec5 100644
--- a/app/scodoc/sco_apogee_csv.py
+++ b/app/scodoc/sco_apogee_csv.py
@@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
-from app.models import FormSemestre
+from app.models import FormSemestre, Identite
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
NAR,
RAT,
)
-from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud
@@ -454,6 +453,12 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid):
"""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
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid)
diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py
index 93a926b6..47947bd3 100644
--- a/app/scodoc/sco_bulletins.py
+++ b/app/scodoc/sco_bulletins.py
@@ -291,15 +291,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
+ u = ue.copy()
+ ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
- ):
+ ) and not ue_status["is_capitalized"]:
+ # saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue
- u = ue.copy()
- ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
+
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT:
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"
else:
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"])
if ue_status["coef_ue"] != None:
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else:
- # C'est un bug:
- log("u=" + pprint.pformat(u))
- raise Exception("invalid None coef for ue")
+ u["coef_ue_txt"] = "-"
if (
dpv
@@ -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)
if intro_mail:
- hea = intro_mail % {
- "nomprenom": etud["nomprenom"],
- "dept": dept,
- "webmaster": webmaster,
- }
+ try:
+ hea = intro_mail % {
+ "nomprenom": etud["nomprenom"],
+ "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:
hea = ""
diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py
index 60c6f2a0..25111f7e 100644
--- a/app/scodoc/sco_bulletins_standard.py
+++ b/app/scodoc/sco_bulletins_standard.py
@@ -284,7 +284,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
)
with_col_moypromo = prefs["bul_show_moypromo"]
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"]
colkeys = ["titre", "module"] # noms des colonnes à afficher
@@ -409,7 +409,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
# Chaque UE:
for ue in I["ues"]:
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"]
rowstyle = ""
plusminus = minuslink #
@@ -592,7 +592,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"_titre_colspan": 2,
"rang": mod["mod_rang_txt"], # vide si pas option rang
"note": mod["mod_moy_txt"],
- "coef": mod["mod_coef_txt"],
+ "coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "",
"abs": mod.get(
"mod_abs_txt", ""
), # absent si pas option show abs module
@@ -656,7 +656,9 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
eval_style = ""
t = {
"module": '
• ' + e["name"],
- "coef": "
" + e["coef_txt"] + "",
+ "coef": ("
" + e["coef_txt"] + "")
+ if prefs["bul_show_coef"]
+ else "",
"_hidden": hidden,
"_module_target": e["target_html"],
# '_module_help' : ,
diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py
index 5fd0cec6..6330849c 100644
--- a/app/scodoc/sco_cache.py
+++ b/app/scodoc/sco_cache.py
@@ -33,17 +33,12 @@
"""
-# API ScoDoc8 pour les caches:
-# sco_cache.NotesTableCache.get( formsemestre_id)
-# => sco_cache.NotesTableCache.get(formsemestre_id)
+# API pour les caches:
+# sco_cache.MyCache.get( formsemestre_id)
+# => sco_cache.MyCache.get(formsemestre_id)
#
-# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None)
-# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False)
-#
-#
-# Nouvelles fonctions:
-# sco_cache.NotesTableCache.delete(formsemestre_id)
-# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
+# sco_cache.MyCache.delete(formsemestre_id)
+# sco_cache.MyCache.delete_many(formsemestre_id_list)
#
# Bulletins PDF:
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
@@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h
-class NotesTableCache(ScoDocCache):
- """Cache pour les NotesTable
- Clé: formsemestre_id
- Valeur: NotesTable instance
- """
-
- prefix = "NT"
-
- @classmethod
- def get(cls, formsemestre_id, compute=True):
- """Returns NotesTable for this formsemestre
- Search in local cache (g.nt_cache) or global app cache (eg REDIS)
- If not in cache:
- If compute is True, build it and cache it
- Else return None
- """
- # try local cache (same request)
- if not hasattr(g, "nt_cache"):
- g.nt_cache = {}
- else:
- if formsemestre_id in g.nt_cache:
- return g.nt_cache[formsemestre_id]
- # try REDIS
- key = cls._get_key(formsemestre_id)
- nt = CACHE.get(key)
- if nt:
- g.nt_cache[formsemestre_id] = nt # cache locally (same request)
- return nt
- if not compute:
- return None
- # Recompute requested table:
- from app.scodoc import notes_table
-
- t0 = time.time()
- nt = notes_table.NotesTable(formsemestre_id)
- t1 = time.time()
- _ = cls.set(formsemestre_id, nt) # cache in REDIS
- t2 = time.time()
- log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)")
- g.nt_cache[formsemestre_id] = nt
- return nt
-
-
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False
):
@@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if not pdfonly:
# Delete cached notes and evaluations
- NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id:
for fid in formsemestre_ids:
EvaluationCache.invalidate_sem(fid)
- if hasattr(g, "nt_cache") and fid in g.nt_cache:
- del g.nt_cache[fid]
+ if (
+ hasattr(g, "formsemestre_results_cache")
+ and fid in g.formsemestre_results_cache
+ ):
+ del g.formsemestre_results_cache[fid]
+
else:
# optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems()
- if hasattr(g, "nt_cache"):
- del g.nt_cache
+ if hasattr(g, "formsemestre_results_cache"):
+ del g.formsemestre_results_cache
SemInscriptionsCache.delete_many(formsemestre_ids)
-
+ ResultatsSemestreCache.delete_many(formsemestre_ids)
+ ValidationsSemestreCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
- ResultatsSemestreCache.delete_many(formsemestre_ids)
- ValidationsSemestreCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager:
diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py
index 59372b8d..bcd6522b 100644
--- a/app/scodoc/sco_codes_parcours.py
+++ b/app/scodoc/sco_codes_parcours.py
@@ -282,7 +282,7 @@ class TypeParcours(object):
return [
ue_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 ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
]
@@ -587,7 +587,7 @@ class ParcoursILEPS(TypeParcours):
# SESSION_ABBRV = 'A' # A1, A2, ...
COMPENSATION_UE = False
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 = 10.0
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")
diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py
index b11f63ca..4b55b41f 100644
--- a/app/scodoc/sco_dump_db.py
+++ b/app/scodoc/sco_dump_db.py
@@ -51,6 +51,7 @@ import fcntl
import subprocess
import requests
+from flask import flash
from flask_login import current_user
import app.scodoc.notesdb as ndb
@@ -124,6 +125,7 @@ def sco_dump_and_send_db():
fcntl.flock(x, fcntl.LOCK_UN)
log("sco_dump_and_send_db: done.")
+ flash("Données envoyées au serveur d'assistance")
return "\n".join(H) + html_sco_header.sco_footer()
@@ -186,18 +188,28 @@ def _send_db(ano_db_name):
log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".dump", dump)}
- r = requests.post(
- scu.SCO_DUMP_UP_URL,
- files=files,
- data={
- "dept_name": sco_preferences.get_preference("DeptName"),
- "serial": _get_scodoc_serial(),
- "sco_user": str(current_user),
- "sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
- "sco_version": sco_version.SCOVERSION,
- "sco_fullversion": scu.get_scodoc_version(),
- },
- )
+ try:
+ r = requests.post(
+ scu.SCO_DUMP_UP_URL,
+ files=files,
+ data={
+ "dept_name": sco_preferences.get_preference("DeptName"),
+ "serial": _get_scodoc_serial(),
+ "sco_user": str(current_user),
+ "sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
+ "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
diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py
index c6b0151d..cec8b7c2 100644
--- a/app/scodoc/sco_edit_apc.py
+++ b/app/scodoc/sco_edit_apc.py
@@ -52,6 +52,7 @@ def html_edit_formation_apc(
"""
parcours = formation.get_parcours()
assert parcours.APC_SAE
+
ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE).order_by(
Module.semestre_id, Module.numero, Module.code
)
@@ -68,6 +69,19 @@ def html_edit_formation_apc(
).order_by(
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] = '
manquant'
+ else:
+ ects_by_sem[semestre_idx] = sum(ects)
+
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
icons = {
@@ -93,7 +107,8 @@ def html_edit_formation_apc(
editable=editable,
tag_editable=tag_editable,
icons=icons,
- UniteEns=UniteEns,
+ ues_by_sem=ues_by_sem,
+ ects_by_sem=ects_by_sem,
),
]
for semestre_idx in semestre_ids:
diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py
index 41a25d09..63bcb72a 100644
--- a/app/scodoc/sco_edit_module.py
+++ b/app/scodoc/sco_edit_module.py
@@ -500,6 +500,13 @@ def module_edit(module_id=None):
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
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 = [
"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...
module_types = (
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 = [
(
"code",
{
"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,
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id, module_id=module_id
@@ -690,7 +701,10 @@ def module_edit(module_id=None):
{
"title": "Code Apogée",
"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,
},
),
diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py
index 17cdc8c0..14e4faec 100644
--- a/app/scodoc/sco_edit_ue.py
+++ b/app/scodoc/sco_edit_ue.py
@@ -29,7 +29,7 @@
"""
import flask
-from flask import url_for, render_template
+from flask import flash, render_template, url_for
from flask import g, request
from flask_login import current_user
@@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
input_formators={
"type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str,
+ "ects": ndb.float_null_is_null,
},
output_formators={
"numero": ndb.int_null_is_zero,
@@ -107,8 +108,6 @@ def ue_list(*args, **kw):
def do_ue_create(args):
"create an ue"
- from app.scodoc import sco_formations
-
cnx = ndb.GetDBConnexion()
# check duplicates
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é !
(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
ue_id = _ueEditor.create(cnx, args)
@@ -128,6 +135,8 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs()
# news
+ ue = UniteEns.query.get(ue_id)
+ flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"])
sco_news.add(
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",
{
- "size": 2,
+ "size": 4,
"explanation": "numéro (1,2,3,4) de l'UE pour l'ordre d'affichage",
"type": "int",
},
@@ -339,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float",
"title": "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"],
},
)
+ flash("UE créée")
else:
do_ue_edit(tf[2])
+ flash("UE modifiée")
return flask.redirect(
url_for(
"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)
ues.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)
# editable = (not locked) and has_perm_change
@@ -664,11 +681,17 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if msg:
H.append('
' + msg + "
")
- if has_duplicate_ue_codes:
+ if ues_with_duplicated_code:
H.append(
- """
Attention: plusieurs UE de cette
- formation ont le même code. Il faut corriger cela ci-dessous,
- sinon les calculs d'ECTS seront erronés !
"""
+ f"""
Attention: plusieurs UE de cette
+ formation ont le même code : {
+ ', '.join([
+ '' + ue["acronyme"] + " (code " + ue["ue_code"] + ")"
+ for ue in ues_with_duplicated_code ])
+ }.
+ Il faut corriger cela, sinon les capitalisations et ECTS seront
+ erronés ! """
)
# Description de la formation
@@ -699,16 +722,16 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
- """
+ """
msg_refcomp = "changer"
H.append(
f"""
- - {descr_refcomp} {msg_refcomp}
- - éditer les coefficients des ressources et SAÉs
@@ -735,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
)
else:
+ H.append('')
H.append(
_ue_table_ues(
parcours,
@@ -764,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
"""
)
-
+ H.append("
")
H.append("
") # formation_ue_list
if ues_externes:
@@ -913,10 +937,10 @@ def _ue_table_ues(
cur_ue_semestre_id = None
iue = 0
for ue in ues:
- if ue["ects"]:
- ue["ects_str"] = ", %g ECTS" % ue["ects"]
- else:
+ if ue["ects"] is None:
ue["ects_str"] = ""
+ else:
+ ue["ects_str"] = ", %g ECTS" % ue["ects"]
if editable:
klass = "span_apo_edit"
else:
@@ -930,8 +954,8 @@ def _ue_table_ues(
if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"]
- if iue > 0:
- H.append("")
+ # if iue > 0:
+ # H.append("")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:"
else:
@@ -953,7 +977,6 @@ def _ue_table_ues(
)
else:
H.append(arrow_none)
- iue += 1
ue["acro_titre"] = str(ue["acronyme"])
if ue["titre"] != ue["acronyme"]:
ue["acro_titre"] += " " + str(ue["titre"])
@@ -1001,6 +1024,14 @@ def _ue_table_ues(
delete_disabled_icon,
)
)
+ if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
+ H.append(
+ f""""""
+ )
+ iue += 1
+
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é !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
-
# On ne peut pas supprimer le code UE:
if "ue_code" in args and not args["ue_code"]:
del args["ue_code"]
diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py
index b784e5f0..21fc2410 100644
--- a/app/scodoc/sco_etape_apogee_view.py
+++ b/app/scodoc/sco_etape_apogee_view.py
@@ -53,7 +53,7 @@ from app.scodoc.sco_exceptions import ScoValueError
def apo_semset_maq_status(
- semset_id="",
+ semset_id: int,
allow_missing_apo=False,
allow_missing_decisions=False,
allow_missing_csv=False,
@@ -65,7 +65,7 @@ def apo_semset_maq_status(
):
"""Page statut / tableau de bord"""
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.fill_formsemestres()
# autorise export meme si etudiants Apo manquants:
diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py
index 09f2179b..d895a8a3 100644
--- a/app/scodoc/sco_evaluations.py
+++ b/app/scodoc/sco_evaluations.py
@@ -405,7 +405,6 @@ def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- sem = formsemestre.to_dict()
evals = nt.get_evaluations_etats()
nb_evals = len(evals)
@@ -416,8 +415,8 @@ def formsemestre_evaluations_cal(formsemestre_id):
today = time.strftime("%Y-%m-%d")
- year = int(sem["annee_debut"])
- if sem["mois_debut_ord"] < 8:
+ year = formsemestre.date_debut.year
+ if formsemestre.date_debut.month < 8:
year -= 1 # calendrier septembre a septembre
events = {} # (day, halfday) : event
for e in evals:
@@ -537,11 +536,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
"""Experimental: un tableau indiquant pour chaque évaluation
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)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- sem = formsemestre.to_dict()
evals = nt.get_evaluations_etats()
T = []
@@ -607,7 +605,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
origin="Généré par %s le " % sco_version.SCONAME
+ 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)
@@ -635,16 +633,13 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
'voir toutes les notes du module'
% moduleimpl_id
)
- mod_descr = (
- '%s %s (resp. %s) %s'
- % (
- moduleimpl_id,
- Mod["code"] or "",
- Mod["titre"] or "?",
- nomcomplet,
- resp,
- link,
- )
+ mod_descr = '%s %s (resp. %s) %s' % (
+ moduleimpl_id,
+ Mod["code"] or "",
+ Mod["titre"] or "?",
+ nomcomplet,
+ resp,
+ link,
)
etit = E["description"] or ""
diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py
index 157cb6e5..4bc039ba 100644
--- a/app/scodoc/sco_find_etud.py
+++ b/app/scodoc/sco_find_etud.py
@@ -39,6 +39,7 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
+from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
@@ -221,7 +222,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip:
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:
code_nip = code_nip or expnom
if code_nip:
diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py
index caeeb707..573c95df 100644
--- a/app/scodoc/sco_formations.py
+++ b/app/scodoc/sco_formations.py
@@ -151,8 +151,14 @@ def formation_export(
if mod["ects"] is None:
del mod["ects"]
+ filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult(
- F, name="formation", format=format, force_outer_xml_tag=False, attached=True
+ F,
+ name="formation",
+ format=format,
+ force_outer_xml_tag=False,
+ attached=True,
+ filename=filename,
)
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 81dbd565..7174cfc3 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -78,7 +78,7 @@ def formsemestre_createwithmodules():
H = [
html_sco_header.sco_header(
page_title="Création d'un semestre",
- javascripts=["libjs/AutoSuggest.js"],
+ javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
),
@@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
- javascripts=["libjs/AutoSuggest.js"],
+ javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
@@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
# en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id]
else:
- semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
+ semestre_id_list = list(range(1, NB_SEM + 1))
+ if not formation.is_apc():
+ # propose "pas de semestre" seulement en classique
+ semestre_id_list.insert(0, -1)
semestre_id_labels = []
for sid in semestre_id_list:
@@ -341,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc()
else "",
+ "attributes": ['onchange="change_semestre_id();"']
+ if formation.is_apc()
+ else "",
},
),
)
@@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
{
"input_type": "boolcheckbox",
"title": "",
- "explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations",
+ "explanation": """Autoriser tous les enseignants associés
+ à un module à y créer des évaluations""",
},
),
(
@@ -534,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False):
]
nbmod = 0
- if edit:
- templ_sep = "%(label)s | Responsable | Inscrire |
"
- else:
- templ_sep = "%(label)s | Responsable |
"
+
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"""%(label)s | Responsable | Inscrire |
"""
+ else:
+ templ_sep = (
+ f"""%(label)s | Responsable |
"""
+ )
modform.append(
(
"sep",
@@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
)
fcg += ""
itemtemplate = (
- """%(label)s | %(elem)s | """
+ f""" |
%(label)s | %(elem)s | """
+ fcg
+ " |
"
)
else:
- itemtemplate = """%(label)s | %(elem)s |
"""
+ itemtemplate = f"""%(label)s | %(elem)s |
"""
modform.append(
(
"MI" + str(mod["module_id"]),
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index d8be56f2..2080e957 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -595,11 +595,12 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
"""Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients
"""
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
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"])
Mlist = sco_moduleimpl.moduleimpl_withmodule_list(
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["evalcomplete_str"] = "Complète"
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(
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):
"""Tableau de bord semestre HTML"""
# porté du DTML
- cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
@@ -1077,7 +1077,7 @@ def formsemestre_status(formsemestre_id=None):
"",
]
- if use_ue_coefs:
+ if use_ue_coefs and not formsemestre.formation.is_apc():
H.append(
"""
utilise les coefficients d'UE pour calculer la moyenne générale.
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index f755bb11..c83e1cc4 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table(
else:
H.append('en cours | ')
H.append('%s | ' % ass) # abs
- # acronymes UEs auxquelles l'étudiant est inscrit:
- # XXX il est probable que l'on doive ici ajouter les
- # XXX UE capitalisées
+ # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion()
+ etud_ue_status = {
+ ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
+ }
ues = [
ue
for ue in ues
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:
@@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"]
else:
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 ""
explanation_ue = [] # list of strings
if code == ADM:
@@ -1250,7 +1252,7 @@ def check_formation_ues(formation_id):
for ue in ues:
# formsemestres utilisant cette ue ?
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
WHERE sem.formation_id = %(formation_id)s
AND mod.id = mi.module_id
@@ -1269,11 +1271,11 @@ def check_formation_ues(formation_id):
return "", {}
# Genere message HTML:
H = [
- """Attention: les UE suivantes de cette formation
+ """
Attention: les UE suivantes de cette formation
sont utilisées dans des
- semestres de rangs différents (eg S1 et S3).
Cela peut engendrer des problèmes pour
- 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),
+ semestres de rangs différents (eg S1 et S3).
Cela peut engendrer des problèmes pour
+ 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 veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
UE extérieure.
@@ -1286,7 +1288,11 @@ def check_formation_ues(formation_id):
for x in ue_multiples[ue["ue_id"]]
]
slist = ", ".join(
- ["%(titreannee)s (semestre %(semestre_id)s)" % s for s in sems]
+ [
+ """%(titreannee)s (semestre %(semestre_id)s)"""
+ % s
+ for s in sems
+ ]
)
H.append("- %s : %s
" % (ue["acronyme"], slist))
H.append("
")
diff --git a/app/scodoc/sco_groups_exports.py b/app/scodoc/sco_groups_exports.py
new file mode 100644
index 00000000..b2e95545
--- /dev/null
+++ b/app/scodoc/sco_groups_exports.py
@@ -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)
diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py
index 5b89ab44..259256bd 100644
--- a/app/scodoc/sco_groups_view.py
+++ b/app/scodoc/sco_groups_view.py
@@ -826,6 +826,8 @@ def tab_absences_html(groups_infos, etat=None):
% groups_infos.groups_query_args,
"""
Liste d'appel avec photos"""
% groups_infos.groups_query_args,
+ """
Liste des annotations"""
+ % groups_infos.groups_query_args,
"",
]
)
diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py
index c489510b..02272ce8 100644
--- a/app/scodoc/sco_logos.py
+++ b/app/scodoc/sco_logos.py
@@ -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
"""
self.logoname = secure_filename(logoname)
+ if not self.logoname:
+ self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
self.scodoc_dept_id = dept_id
self.prefix = prefix or ""
if self.scodoc_dept_id:
@@ -276,7 +278,7 @@ class Logo:
if self.mm is None:
return f'
'
else:
- return f''
+ return f''
def last_modified(self):
path = Path(self.filepath)
diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py
index e3c23230..453f63f1 100644
--- a/app/scodoc/sco_moduleimpl_inscriptions.py
+++ b/app/scodoc/sco_moduleimpl_inscriptions.py
@@ -305,7 +305,10 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
if can_change:
c_link = (
'%s'
- % (mod["moduleimpl_id"], mod["descri"])
+ % (
+ mod["moduleimpl_id"],
+ mod["descri"] or "(inscrire des étudiants)",
+ )
)
else:
c_link = mod["descri"]
diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py
index d6e244e7..fbf190c5 100644
--- a/app/scodoc/sco_parcours_dut.py
+++ b/app/scodoc/sco_parcours_dut.py
@@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object):
# pour le DUT, le dernier est toujours S4.
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
# (licences et autres formations en 1 seule session))
- self.semestre_non_terminal = (
- self.sem["semestre_id"] != self.parcours.NB_SEM
- ) # True | False
+ self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant:
diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py
index 0e099680..3949f50a 100755
--- a/app/scodoc/sco_pdf.py
+++ b/app/scodoc/sco_pdf.py
@@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate):
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None
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:
# Also try to use PV background
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:
self.background_image_filename = logo.filepath
diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index b639d5ee..aab148e7 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -1296,11 +1296,21 @@ class BasePreferences(object):
"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",
{
"initvalue": 1,
- "title": "Afficher coefficient des ue/modules sur les bulletins",
+ "title": "Afficher coefficient des modules sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],
diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py
index 1fb98e3d..2e8cdaf6 100644
--- a/app/scodoc/sco_pvpdf.py
+++ b/app/scodoc/sco_pvpdf.py
@@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate):
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
+ ) or find_logo(
+ logoname="pvjury_background",
+ dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
+ ) or find_logo(
+ logoname="letter_background",
+ dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:
diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
index bbcf4a08..db775438 100644
--- a/app/scodoc/sco_synchro_etuds.py
+++ b/app/scodoc/sco_synchro_etuds.py
@@ -854,23 +854,27 @@ def formsemestre_import_etud_admission(
apo_emailperso = etud.get("mailperso", "")
if info["emailperso"] and not apo_emailperso:
apo_emailperso = info["emailperso"]
- if (
- import_email
- and info["email"] != etud["mail"]
- or info["emailperso"] != apo_emailperso
- ):
- sco_etud.adresse_edit(
- cnx,
- args={
- "etudid": etudid,
- "adresse_id": info["adresse_id"],
- "email": etud["mail"],
- "emailperso": apo_emailperso,
- },
- )
- # notifie seulement les changements d'adresse mail institutionnelle
- if info["email"] != etud["mail"]:
- changed_mails.append((info, etud["mail"]))
+ if import_email:
+ if not "mail" in etud:
+ raise ScoValueError(
+ "la réponse portail n'a pas le champs requis 'mail'"
+ )
+ if (
+ info["email"] != etud["mail"]
+ or info["emailperso"] != apo_emailperso
+ ):
+ sco_etud.adresse_edit(
+ cnx,
+ args={
+ "etudid": etudid,
+ "adresse_id": info["adresse_id"],
+ "email": etud["mail"],
+ "emailperso": apo_emailperso,
+ },
+ )
+ # notifie seulement les changements d'adresse mail institutionnelle
+ if info["email"] != etud["mail"]:
+ changed_mails.append((info, etud["mail"]))
else:
unknowns.append(code_nip)
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])
diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py
index 1cc6e89c..196c2c21 100644
--- a/app/scodoc/sco_utils.py
+++ b/app/scodoc/sco_utils.py
@@ -50,7 +50,7 @@ import pydot
import requests
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 app import log
@@ -616,6 +616,16 @@ def bul_filename(sem, etud, format):
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
"""publication fichier CSV."""
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)
-def sendJSON(data, attached=False):
+def sendJSON(data, attached=False, filename=None):
js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
return send_file(
- js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached
+ js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached
)
-def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True):
+def sendXML(
+ data,
+ tagname=None,
+ force_outer_xml_tag=True,
+ attached=False,
+ quote=True,
+ filename=None,
+):
if type(data) != list:
data = [data] # always list-of-dicts
if force_outer_xml_tag:
data = [{tagname: data}]
tagname += "_list"
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote)
- return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached)
+ return send_file(
+ doc, filename=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached
+ )
def sendResult(
@@ -659,6 +678,7 @@ def sendResult(
force_outer_xml_tag=True,
attached=False,
quote_xml=True,
+ filename=None,
):
if (format is None) or (format == "html"):
return data
@@ -669,9 +689,10 @@ def sendResult(
force_outer_xml_tag=force_outer_xml_tag,
attached=attached,
quote=quote_xml,
+ filename=filename,
)
elif format == "json":
- return sendJSON(data, attached=attached)
+ return sendJSON(data, attached=attached, filename=filename)
else:
raise ValueError("invalid format: %s" % format)
@@ -789,7 +810,7 @@ def abbrev_prenom(prenom):
#
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")
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index e8893085..d735077a 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -138,7 +138,7 @@ div.head_message {
border-radius: 8px;
font-family : arial, verdana, sans-serif ;
font-weight: bold;
- width: 40%;
+ width: 70%;
text-align: center;
}
@@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */
}
div.logo-logo {
+ margin-left: -5px;
text-align: center ;
}
div.logo-logo img {
box-sizing: content-box;
- margin-top: -10px;
- width: 128px;
+ margin-top: 10px; /* -10px */
+ width: 135px; /* 128px */
padding-right: 5px;
- margin-left: -75px;
}
div.sidebar-bottom {
margin-top: 10px;
@@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits {
text-align: center;
}
td.formsemestre_status_code {
- width: 2em;
+ /* width: 2em; */
padding-right: 1em;
}
@@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list {
padding-top: 5px;
padding-bottom: 5px;
}
-
+span.missing_ue_ects {
+ color: red;
+ font-weight: bold;
+}
li.module_malus span.formation_module_tit {
color: red;
font-weight: bold;
@@ -1703,10 +1706,16 @@ ul.notes_ue_list {
padding-bottom: 1em;
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;
list-style-type: none;
+ border: 1px solid maroon;
+ border-radius: 10px;
+ padding-bottom: 5px;
}
span.ue_type_1 {
color: green;
@@ -1749,6 +1758,7 @@ ul.notes_matiere_list {
background-color: rgb(220,220,220);
font-weight: normal;
font-style: italic;
+ border-top: 1px solid maroon;
}
ul.notes_module_list {
@@ -1757,6 +1767,11 @@ ul.notes_module_list {
font-style: normal;
}
+div.ue_list_tit_sem {
+ font-size: 120%;
+ font-weight: bold;
+}
+
.notes_ue_list a.stdlink {
color: #001084;
text-decoration: underline;
diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png
index fb0467ae..5c710a76 100644
Binary files a/app/static/icons/scologo_img.png and b/app/static/icons/scologo_img.png differ
diff --git a/app/static/js/formsemestre_edit.js b/app/static/js/formsemestre_edit.js
new file mode 100644
index 00000000..3394d659
--- /dev/null
+++ b/app/static/js/formsemestre_edit.js
@@ -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();
+});
\ No newline at end of file
diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js
index 5dcd9e5b..dcc4520b 100644
--- a/app/static/js/releve-but.js
+++ b/app/static/js/releve-but.js
@@ -91,7 +91,7 @@ class releveBUT extends HTMLElement {
Inscrit le
-
Les moyennes servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.
+
Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.
diff --git a/app/templates/base.html b/app/templates/base.html
index 176e4b99..658f41eb 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -61,12 +61,10 @@
{% block content %}
- {% with messages = get_flashed_messages() %}
- {% if messages %}
- {% for message in messages %}
-
{{ message }}
- {% endfor %}
- {% endif %}
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% for category, message in messages %}
+
{{ message }}
+ {% endfor %}
{% endwith %}
{# application content needs to be provided in the app_content block #}
diff --git a/app/templates/but/refcomp_load.html b/app/templates/but/refcomp_load.html
index 290de937..50d73d80 100644
--- a/app/templates/but/refcomp_load.html
+++ b/app/templates/but/refcomp_load.html
@@ -18,10 +18,12 @@
Liste des référentiels de compétences chargés
+ {% if formation is not none %}
Association à la formation {{ formation.acronyme }}
+ {% endif %}
diff --git a/app/templates/flashed_messages.html b/app/templates/flashed_messages.html
new file mode 100644
index 00000000..5ded7524
--- /dev/null
+++ b/app/templates/flashed_messages.html
@@ -0,0 +1,9 @@
+{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #}
+{# -*- mode: jinja-html -*- #}
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% for category, message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endwith %}
+
\ No newline at end of file
diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html
index 2be3cfe2..10927bc8 100644
--- a/app/templates/pn/form_mods.html
+++ b/app/templates/pn/form_mods.html
@@ -65,6 +65,13 @@
{% endfor %}
+ {% if mod.ue.type != 0 and mod.module_type != 0 %}
+
+ type incompatible avec son UE de rattachement !
+
+ {% endif %}
+
diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html
index 350d12d8..a91f2942 100644
--- a/app/templates/pn/form_ues.html
+++ b/app/templates/pn/form_ues.html
@@ -3,11 +3,9 @@