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

This commit is contained in:
Emmanuel Viennet 2022-02-19 16:17:51 +01:00
commit f2c3841db9
32 changed files with 422 additions and 158 deletions

View File

@ -13,6 +13,7 @@ Les classes de Bonus fournissent deux méthodes:
""" """
import datetime import datetime
import math
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -52,7 +53,7 @@ class BonusSport:
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs). etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
""" """
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen reste None) # En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen est ajusté pour le prendre en compte)
classic_use_bonus_ues = False classic_use_bonus_ues = False
# Attributs virtuels: # Attributs virtuels:
@ -106,6 +107,8 @@ class BonusSport:
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport) # sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus) # ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2] nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
if nb_etuds == 0 or nb_mod_sport == 0:
return # no bonus at all
# Enlève les NaN du numérateur: # Enlève les NaN du numérateur:
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0) sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
@ -157,7 +160,8 @@ class BonusSport:
"""Calcul des bonus: méthode virtuelle à écraser. """Calcul des bonus: méthode virtuelle à écraser.
Arguments: Arguments:
- sem_modimpl_moys_inscrits: - sem_modimpl_moys_inscrits:
ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue_non_bonus) ndarray (nb_etuds, mod_sport)
ou en APC (nb_etuds, mods_sport, nb_ue_non_bonus)
les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans. les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
- modimpl_coefs_etuds_no_nan: - modimpl_coefs_etuds_no_nan:
les coefficients: float ndarray les coefficients: float ndarray
@ -201,7 +205,8 @@ class BonusSportAdditif(BonusSport):
"""calcul du bonus """calcul du bonus
sem_modimpl_moys_inscrits: les notes de sport sem_modimpl_moys_inscrits: les notes de sport
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus) En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
modimpl_coefs_etuds_no_nan: En classic: ndarray (nb_etuds, nb_mod_sport)
modimpl_coefs_etuds_no_nan: même shape, les coefs.
""" """
if 0 in sem_modimpl_moys_inscrits.shape: if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module... # pas d'étudiants ou pas d'UE ou pas de module...
@ -224,22 +229,28 @@ class BonusSportAdditif(BonusSport):
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr) bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus) # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues: if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale # Bonus sur les UE et None sur moyenne générale
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)] ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame( self.bonus_ues = pd.DataFrame(
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
) )
elif self.classic_use_bonus_ues:
# Formations classiques apppliquant le bonus sur les UEs
# ici bonus_moy_arr = ndarray 1d nb_etuds
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues_idx)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
else: else:
# Bonus sur la moyenne générale seulement # Bonus sur la moyenne générale seulement
self.bonus_moy_gen = pd.Series( self.bonus_moy_gen = pd.Series(
bonus_moy_arr, index=self.etuds_idx, dtype=float bonus_moy_arr, index=self.etuds_idx, dtype=float
) )
# if len(bonus_moy_arr.shape) > 1:
# bonus_moy_arr = bonus_moy_arr.sum(axis=1)
# Laisse bonus_moy_gen à None, en APC le bonus moy. gen. sera réparti sur les UEs.
class BonusSportMultiplicatif(BonusSport): class BonusSportMultiplicatif(BonusSport):
"""Bonus sport qui multiplie les moyennes d'UE par un facteur""" """Bonus sport qui multiplie les moyennes d'UE par un facteur"""
@ -284,6 +295,7 @@ class BonusSportMultiplicatif(BonusSport):
class BonusDirect(BonusSportAdditif): class BonusDirect(BonusSportAdditif):
"""Bonus direct: les points sont directement ajoutés à la moyenne générale. """Bonus direct: les points sont directement ajoutés à la moyenne générale.
Les coefficients sont ignorés: tous les points de bonus sont sommés. Les coefficients sont ignorés: tous les points de bonus sont sommés.
(rappel: la note est ramenée sur 20 avant application). (rappel: la note est ramenée sur 20 avant application).
""" """
@ -294,8 +306,68 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0 proportion_point = 1.0
class BonusAmiens(BonusSportAdditif):
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,1 point
sur toutes les moyennes d'UE.
"""
name = "bonus_amiens"
displayed_name = "IUT d'Amiens"
seuil_moy_gen = 0.0 # tous les points sont comptés
proportion_point = 1e10
bonus_max = 0.1
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
# Finalement ils n'en veulent pas.
# class BonusAnnecy(BonusSport):
# """Calcul bonus modules optionnels (sport), règle IUT d'Annecy.
# Il peut y avoir plusieurs modules de bonus.
# Prend pour chaque étudiant la meilleure de ses notes bonus et
# ajoute à chaque UE :<br>
# 0.05 point si >=10,<br>
# 0.1 point si >=12,<br>
# 0.15 point si >=14,<br>
# 0.2 point si >=16,<br>
# 0.25 point si >=18.
# """
# name = "bonus_iut_annecy"
# displayed_name = "IUT d'Annecy"
# def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
# """calcul du bonus"""
# # if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
# # return # no etuds or no mod sport
# # Prend la note de chaque modimpl, sans considération d'UE
# if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
# sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
# note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
# bonus = np.zeros(note_bonus_max.shape)
# bonus[note_bonus_max >= 10.0] = 0.05
# bonus[note_bonus_max >= 12.0] = 0.10
# bonus[note_bonus_max >= 14.0] = 0.15
# bonus[note_bonus_max >= 16.0] = 0.20
# bonus[note_bonus_max >= 18.0] = 0.25
# # Bonus moyenne générale et sur les UE
# self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
# ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
# nb_ues_no_bonus = len(ues_idx)
# self.bonus_ues = pd.DataFrame(
# np.stack([bonus] * nb_ues_no_bonus, axis=1),
# columns=ues_idx,
# index=self.etuds_idx,
# dtype=float,
# )
class BonusBethune(BonusSportMultiplicatif): class BonusBethune(BonusSportMultiplicatif):
"""Calcul bonus modules optionels (sport), règle IUT de Béthune. """Calcul bonus modules optionnels (sport), règle IUT de Béthune.
Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre. Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
Ce bonus est égal au nombre de points divisé par 200 et multiplié par la Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
@ -309,7 +381,7 @@ class BonusBethune(BonusSportMultiplicatif):
class BonusBezier(BonusSportAdditif): class BonusBezier(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT de Bézier. """Calcul bonus modules optionnels (sport, culture), règle IUT de Bézier.
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
sport , etc) non rattachés à une unité d'enseignement. Les points sport , etc) non rattachés à une unité d'enseignement. Les points
@ -330,28 +402,91 @@ class BonusBezier(BonusSportAdditif):
class BonusBordeaux1(BonusSportMultiplicatif): class BonusBordeaux1(BonusSportMultiplicatif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale """Calcul bonus modules optionnels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale
et UE. et UE.
<p>
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement. de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
</p><p>
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un % Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
qui augmente la moyenne de chaque UE et la moyenne générale. qui augmente la moyenne de chaque UE et la moyenne générale.<br>
Formule : le % = points>moyenne / 2 Formule : pourcentage = (points au dessus de 10) / 2
</p><p>
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale. Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
</p>
""" """
name = "bonus_iutBordeaux1" name = "bonus_iutBordeaux1"
displayed_name = "IUT de Bordeaux 1" displayed_name = "IUT de Bordeaux"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 10.0 seuil_moy_gen = 10.0
amplitude = 0.005 amplitude = 0.005
class BonusCachan1(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1.
<ul>
<li> DUT/LP : la meilleure note d'option, si elle est supérieure à 10,
bonifie les moyennes d'UE (<b>sauf l'UE41 dont le code est UE41_E</b>) à raison
de <em>bonus = (option - 10)/10</em>.
</li>
<li> BUT : la meilleure note d'option, si elle est supérieure à 10, bonifie
les moyennes d'UE à raison de <em>bonus = (option - 10)*5%</em>.</li>
</ul>
"""
name = "bonus_cachan1"
displayed_name = "IUT de Cachan 1"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.05
classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant le type de formation"""
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
else: # --- DUT
# pareil mais proportion différente et exclusion d'une UE
proportion_point = 0.1
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
# Pas de bonus sur la ou les ue de code "UE41_E"
ue_exclues = [ue for ue in ues if ue.ue_code == "UE41_E"]
for ue in ue_exclues:
self.bonus_ues[ue.id] = 0.0
class BonusColmar(BonusSportAdditif): class BonusColmar(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Colmar. """Calcul bonus modules optionnels (sport, culture), règle IUT Colmar.
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non
@ -374,19 +509,21 @@ class BonusColmar(BonusSportAdditif):
class BonusGrenobleIUT1(BonusSportMultiplicatif): class BonusGrenobleIUT1(BonusSportMultiplicatif):
"""Bonus IUT1 de Grenoble """Bonus IUT1 de Grenoble
<p>
À compter de sept. 2021: À compter de sept. 2021:
La note de sport est sur 20, et on calcule une bonification (en %) La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
la formule : bonification (en %) = (note-10)*0,5. la formule : bonification (en %) = (note-10)*0,5.
</p><p>
Bonification qui ne s'applique que si la note est >10. <em>La bonification ne s'applique que si la note est supérieure à 10.</em>
</p><p>
(Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif) (Une note de 10 donne donc 0% de bonif, et une note de 20 : 5% de bonif)
</p><p>
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20). Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
Chaque point correspondait à 0.25% d'augmentation de la moyenne Chaque point correspondait à 0.25% d'augmentation de la moyenne
générale. générale.
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%. Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
</p>
""" """
name = "bonus_iut1grenoble_2017" name = "bonus_iut1grenoble_2017"
@ -411,11 +548,13 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
class BonusLaRochelle(BonusSportAdditif): class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT de La Rochelle. """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point. <ul>
Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette <li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li>
note sur la moyenne générale du semestre (ou sur les UE en BUT). <li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li>
</ul>
""" """
name = "bonus_iutlr" name = "bonus_iutlr"
@ -440,16 +579,17 @@ class BonusLeHavre(BonusSportMultiplicatif):
class BonusLeMans(BonusSportAdditif): class BonusLeMans(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans. """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières <p>Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés. optionnelles sont cumulés.
</p>
<ul>
<li>En BUT: la moyenne de chacune des UE du semestre est augmentée de
2% du cumul des points de bonus;</li>
<li>En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
En BUT: la moyenne de chacune des UE du semestre est augmentée de </li>
2% du cumul des points de bonus, </ul>
<p>Dans tous les cas, le bonus est dans la limite de 0,5 point.</p>
En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
Dans tous les cas, le bonus est dans la limite de 0,5 point.
""" """
name = "bonus_iutlemans" name = "bonus_iutlemans"
@ -471,14 +611,15 @@ class BonusLeMans(BonusSportAdditif):
# Bonus simple, mais avec changement de paramètres en 2010 ! # Bonus simple, mais avec changement de paramètres en 2010 !
class BonusLille(BonusSportAdditif): class BonusLille(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq """Calcul bonus modules optionnels (sport, culture), règle IUT Villeneuve d'Ascq
Les étudiants de l'IUT peuvent suivre des enseignements optionnels <p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement. de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement.
</p><p>
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant. s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
</p>
""" """
name = "bonus_lille" name = "bonus_lille"
@ -530,17 +671,19 @@ class BonusMulhouse(BonusSportAdditif):
class BonusNantes(BonusSportAdditif): class BonusNantes(BonusSportAdditif):
"""IUT de Nantes (Septembre 2018) """IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification <p>Nous avons différents types de bonification
(sport, culture, engagement citoyen). (sport, culture, engagement citoyen).
</p><p>
Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item
la bonification totale ne doit pas excéder les 0,5 point. la bonification totale ne doit pas excéder les 0,5 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications. Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
</p><p>
Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura des modules Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura
pour chaque activité (Sport, Associations, ...) des modules pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20,
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale) mais en fait ce sera la valeur de la bonification: entrer 0,1/20 signifiera
un bonus de 0,1 point la moyenne générale).
</p>
""" """
name = "bonus_nantes" name = "bonus_nantes"
@ -561,11 +704,11 @@ class BonusRoanne(BonusSportAdditif):
displayed_name = "IUT de Roanne" displayed_name = "IUT de Roanne"
seuil_moy_gen = 0.0 seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points bonus_max = 0.6 # plafonnement à 0.6 points
apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP classic_use_bonus_ues = True # sur les UE, même en DUT et LP
class BonusStDenis(BonusSportAdditif): class BonusStDenis(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Saint-Denis """Calcul bonus modules optionnels (sport, culture), règle IUT Saint-Denis
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 13 (sports, musique, deuxième langue, de l'Université Paris 13 (sports, musique, deuxième langue,
@ -584,13 +727,14 @@ class BonusStDenis(BonusSportAdditif):
class BonusTours(BonusDirect): class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours. """Calcul bonus sport & culture IUT Tours.
Les notes des UE bonus (ramenées sur 20) sont sommées <p>Les notes des UE bonus (ramenées sur 20) sont sommées
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale, et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
soit pour le BUT à chaque moyenne d'UE. soit pour le BUT à chaque moyenne d'UE.
</p><p>
Attention: en GEII, facteur 1/40, ailleurs facteur 1. <em>Attention: en GEII, facteur 1/40, ailleurs facteur 1.</em>
</p><p>
Le bonus total est limité à 1 point. Le bonus total est limité à 1 point.
</p>
""" """
name = "bonus_tours" name = "bonus_tours"
@ -611,15 +755,17 @@ class BonusTours(BonusDirect):
class BonusVilleAvray(BonusSport): class BonusVilleAvray(BonusSport):
"""Bonus modules optionels (sport, culture), règle IUT Ville d'Avray. """Bonus modules optionnels (sport, culture), règle IUT Ville d'Avray.
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement. de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
Si la note est >= 10 et < 12, bonus de 0.1 point <ul>
Si la note est >= 12 et < 16, bonus de 0.2 point <li>Si la note est >= 10 et < 12, bonus de 0.1 point</li>
Si la note est >= 16, bonus de 0.3 point <li>Si la note est >= 12 et < 16, bonus de 0.2 point</li>
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par <li>Si la note est >= 16, bonus de 0.3 point</li>
l'étudiant. </ul>
<p>Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant.</p>
""" """
name = "bonus_iutva" name = "bonus_iutva"
@ -645,7 +791,7 @@ class BonusVilleAvray(BonusSport):
class BonusIUTV(BonusSportAdditif): class BonusIUTV(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse """Calcul bonus modules optionnels (sport, culture), règle IUT Villetaneuse
Les étudiants de l'IUT peuvent suivre des enseignements optionnels Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 13 (sports, musique, deuxième langue, de l'Université Paris 13 (sports, musique, deuxième langue,
@ -657,7 +803,7 @@ class BonusIUTV(BonusSportAdditif):
name = "bonus_iutv" name = "bonus_iutv"
displayed_name = "IUT de Villetaneuse" displayed_name = "IUT de Villetaneuse"
pass # oui, c'ets le bonus par défaut pass # oui, c'est le bonus par défaut
def get_bonus_class_dict(start=BonusSport, d=None): def get_bonus_class_dict(start=BonusSport, d=None):

View File

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

View File

@ -218,21 +218,25 @@ def compute_ue_moys_apc(
ues: list, ues: list,
modimpl_inscr_df: pd.DataFrame, modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame,
modimpl_mask: np.array,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Calcul de la moyenne d'UE en mode APC (BUT). """Calcul de la moyenne d'UE en mode APC (BUT).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici] ERR erreur dans une formule utilisateurs (pas gérées ici).
sem_cube: notes moyennes aux modules sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs) ndarray (etuds x modimpls x UEs)
(floats avec des NaN) (floats avec des NaN)
etuds : liste des étudiants (dim. 0 du cube) etuds : liste des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube) modimpls : liste des module_impl (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube) ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
(utilisé pour éliminer les bonus, et pourra servir à cacluler
sur des sous-ensembles de modules)
Résultat: DataFrame columns UE (sans bonus), rows etudid Résultat: DataFrame columns UE (sans bonus), rows etudid
""" """
@ -249,7 +253,8 @@ def compute_ue_moys_apc(
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values # Met à zéro tous les coefs des modules non sélectionnés dans le masque:
modimpl_coefs = np.where(modimpl_mask, modimpl_coefs_df.values, 0.0)
# Duplique les inscriptions sur les UEs non bonus: # Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2) modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
@ -266,6 +271,8 @@ def compute_ue_moys_apc(
) )
# Annule les coefs des modules NaN # Annule les coefs des modules NaN
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 0.0, modimpl_coefs_etuds) modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 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)
# #
# Version vectorisée # Version vectorisée
# #
@ -348,7 +355,8 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan = np.where( modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds 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)
# --------------------- Calcul des moyennes d'UE # --------------------- Calcul des moyennes d'UE
ue_modules = np.array( ue_modules = np.array(
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues] [[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
@ -358,6 +366,8 @@ def compute_ue_moys_classic(
) )
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2) coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = ( etud_moy_ue = (
np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2) np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2)

View File

@ -56,14 +56,11 @@ class ResultatsSemestreBUT(NotesTableCompat):
# modimpl_coefs_df.columns.get_loc(modimpl.id) # modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
# Elimine les coefs des modimpl bonus sports: # Masque de tous les modules _sauf_ les bonus (sport)
modimpls_sport = [ modimpls_mask = [
modimpl modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.type == UE_SPORT
] ]
for modimpl in modimpls_sport:
self.modimpl_coefs_df[modimpl.id] = 0
self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube, self.sem_cube,
@ -72,6 +69,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.ues, self.ues,
self.modimpl_inscr_df, self.modimpl_inscr_df,
self.modimpl_coefs_df, self.modimpl_coefs_df,
modimpls_mask,
) )
# Les coefficients d'UE ne sont pas utilisés en APC # Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame( self.etud_coef_ue_df = pd.DataFrame(
@ -85,7 +83,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_moy_ue -= self.malus self.etud_moy_ue -= self.malus
# --- Bonus Sport & Culture # --- Bonus Sport & Culture
if len(modimpls_sport) > 0: if not all(modimpls_mask): # au moins un module bonus
bonus_class = ScoDocSiteConfig.get_bonus_sport_class() bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None: if bonus_class is not None:
bonus: BonusSport = bonus_class( bonus: BonusSport = bonus_class(

View File

@ -60,7 +60,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
) )
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs = np.array( self.modimpl_coefs = np.array(
[m.module.coefficient for m in self.formsemestre.modimpls_sorted] [m.module.coefficient or 0.0 for m in self.formsemestre.modimpls_sorted]
) )
self.modimpl_idx = { self.modimpl_idx = {
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted) m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
@ -113,11 +113,20 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.etud_moy_ue += self.bonus_ues # somme les dataframes self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
bonus_mg = bonus.get_bonus_moy_gen() bonus_mg = bonus.get_bonus_moy_gen()
if bonus_mg is not None: if bonus_mg is None and self.bonus_ues is not None:
# pas de bonus explicite sur la moyenne générale
# on l'ajuste pour refléter les modifs d'UE, à l'aide des coefs d'UE.
bonus_mg = (self.etud_coef_ue_df * self.bonus_ues).sum(
axis=1
) / self.etud_coef_ue_df.sum(axis=1)
self.etud_moy_gen += bonus_mg self.etud_moy_gen += bonus_mg
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True) elif bonus_mg is not None:
# compat nt, utilisé pour l'afficher sur les bulletins: # Applique le bonus moyenne générale renvoyé
self.bonus = bonus_mg self.etud_moy_gen += bonus_mg
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
# --- UE capitalisées # --- UE capitalisées
self.apply_capitalisation() self.apply_capitalisation()

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ class Module(db.Model):
# id de l'element pedagogique Apogee correspondant: # id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum) # Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
module_type = db.Column(db.Integer) module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations: # Relations:
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True) ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)

View File

@ -298,6 +298,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs" u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
else: else:
u["cur_moy_ue_txt"] = "bonus de %.3g points" % x u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
if nt.bonus_ues is not None:
u["cur_moy_ue_txt"] += " (+ues)"
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
if ue_status["coef_ue"] != None: if ue_status["coef_ue"] != None:
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"]) u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])

View File

@ -500,6 +500,13 @@ def module_edit(module_id=None):
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
if is_apc: if is_apc:
# ne conserve que la 1ere matière de chaque UE,
# et celle à laquelle ce module est rattaché
matieres = [
mat
for mat in matieres
if a_module.matiere.id == mat.id or mat.id == mat.ue.matieres.first().id
]
mat_names = [ mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
] ]

View File

@ -601,7 +601,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
_add_ue_semestre_id(ues_externes, is_apc) _add_ue_semestre_id(ues_externes, is_apc)
ues.sort(key=lambda u: (u["semestre_id"], u["numero"])) ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"])) ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"]))
has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues) # Codes dupliqués (pour aider l'utilisateur)
seen = set()
duplicated_codes = {
ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"])
}
ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes]
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation) has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and has_perm_change # editable = (not locked) and has_perm_change
@ -664,11 +669,17 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if msg: if msg:
H.append('<p class="msg">' + msg + "</p>") H.append('<p class="msg">' + msg + "</p>")
if has_duplicate_ue_codes: if ues_with_duplicated_code:
H.append( H.append(
"""<div class="ue_warning"><span>Attention: plusieurs UE de cette f"""<div class="ue_warning"><span>Attention: plusieurs UE de cette
formation ont le même code. Il faut corriger cela ci-dessous, formation ont le même code : <tt>{
sinon les calculs d'ECTS seront erronés !</span></div>""" ', '.join([
'<a class="stdlink" href="' + url_for( "notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"] )
+ '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>"
for ue in ues_with_duplicated_code ])
}</tt>.
Il faut corriger cela, sinon les capitalisations et ECTS seront
erronés !</span></div>"""
) )
# Description de la formation # Description de la formation
@ -930,8 +941,8 @@ def _ue_table_ues(
if cur_ue_semestre_id != ue["semestre_id"]: if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"] cur_ue_semestre_id = ue["semestre_id"]
if iue > 0: # if iue > 0:
H.append("</ul>") # H.append("</ul>")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:" lab = "Pas d'indication de semestre:"
else: else:
@ -953,7 +964,6 @@ def _ue_table_ues(
) )
else: else:
H.append(arrow_none) H.append(arrow_none)
iue += 1
ue["acro_titre"] = str(ue["acronyme"]) ue["acro_titre"] = str(ue["acronyme"])
if ue["titre"] != ue["acronyme"]: if ue["titre"] != ue["acronyme"]:
ue["acro_titre"] += " " + str(ue["titre"]) ue["acro_titre"] += " " + str(ue["titre"])
@ -1001,6 +1011,14 @@ def _ue_table_ues(
delete_disabled_icon, delete_disabled_icon,
) )
) )
if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]:
H.append(
f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
formation_id=ue['formation_id'], semestre_idx=ue['semestre_id'])
}">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>"""
)
iue += 1
return "\n".join(H) return "\n".join(H)

View File

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

View File

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

View File

@ -1250,7 +1250,7 @@ def check_formation_ues(formation_id):
for ue in ues: for ue in ues:
# formsemestres utilisant cette ue ? # formsemestres utilisant cette ue ?
sems = ndb.SimpleDictFetch( sems = ndb.SimpleDictFetch(
"""SELECT DISTINCT sem.id AS formsemestre_id, sem.* """SELECT DISTINCT sem.id AS formsemestre_id, sem.*
FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi
WHERE sem.formation_id = %(formation_id)s WHERE sem.formation_id = %(formation_id)s
AND mod.id = mi.module_id AND mod.id = mi.module_id
@ -1269,11 +1269,11 @@ def check_formation_ues(formation_id):
return "", {} return "", {}
# Genere message HTML: # Genere message HTML:
H = [ H = [
"""<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation """<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
sont utilisées dans des sont utilisées dans des
semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour semestres de rangs différents (eg S1 et S3). <br/>Cela peut engendrer des problèmes pour
la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation: la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation:
soit modifier le programme de la formation (définir des UE dans chaque semestre), soit modifier le programme de la formation (définir des UE dans chaque semestre),
soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une
UE extérieure. UE extérieure.
<ul> <ul>
@ -1286,7 +1286,11 @@ def check_formation_ues(formation_id):
for x in ue_multiples[ue["ue_id"]] for x in ue_multiples[ue["ue_id"]]
] ]
slist = ", ".join( slist = ", ".join(
["%(titreannee)s (<em>semestre %(semestre_id)s</em>)" % s for s in sems] [
"""%(titreannee)s (<em>semestre <b class="fontred">%(semestre_id)s</b></em>)"""
% s
for s in sems
]
) )
H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist)) H.append("<li><b>%s</b> : %s</li>" % (ue["acronyme"], slist))
H.append("</ul></div>") H.append("</ul></div>")

View File

@ -302,7 +302,12 @@ class DisplayedGroupsInfos(object):
if group_ids: if group_ids:
group_ids = [group_ids] # cas ou un seul parametre, pas de liste group_ids = [group_ids] # cas ou un seul parametre, pas de liste
else: else:
group_ids = [int(g) for g in group_ids] try:
group_ids = [int(g) for g in group_ids]
except ValueError as exc:
raise ScoValueError(
"identifiant de groupe invalide (mettre à jour vos bookmarks ?)"
) from exc
if not formsemestre_id and moduleimpl_id: if not formsemestre_id and moduleimpl_id:
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id) mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1: if len(mods) != 1:

View File

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

View File

@ -1707,6 +1707,9 @@ ul.notes_ue_list {
li.notes_ue_list { li.notes_ue_list {
margin-top: 9px; margin-top: 9px;
list-style-type: none; list-style-type: none;
border: 1px solid maroon;
border-radius: 10px;
padding-bottom: 5px;
} }
span.ue_type_1 { span.ue_type_1 {
color: green; color: green;
@ -1749,6 +1752,7 @@ ul.notes_matiere_list {
background-color: rgb(220,220,220); background-color: rgb(220,220,220);
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
border-top: 1px solid maroon;
} }
ul.notes_module_list { ul.notes_module_list {

View File

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

View File

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

View File

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

View File

@ -48,6 +48,9 @@
}}">modifier</a> }}">modifier</a>
{% endif %} {% endif %}
{% if ue.type == 1 and ue.modules.count() == 0 %}
<span class="warning" title="pas de module, donc pas de bonus calculé">aucun module rattaché !</span>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

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

View File

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

View File

@ -397,7 +397,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""):
) )
@bp.route("/ue_infos/<ue_id>") @bp.route("/ue_infos/<int:ue_id>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def ue_infos(ue_id): def ue_infos(ue_id):

View File

@ -68,7 +68,7 @@ from app.scodoc.sco_permissions import Permission
@bp.route("/table_modules_ue_coefs/<int:formation_id>") @bp.route("/table_modules_ue_coefs/<int:formation_id>")
@bp.route("/table_modules_ue_coefs/<int:formation_id>/<semestre_idx>") @bp.route("/table_modules_ue_coefs/<int:formation_id>/<int:semestre_idx>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def table_modules_ue_coefs(formation_id, semestre_idx=None): def table_modules_ue_coefs(formation_id, semestre_idx=None):

View File

@ -121,7 +121,7 @@ def create_dept():
) )
@bp.route("/ScoDoc/toggle_dept_vis/<dept_id>", methods=["GET", "POST"]) @bp.route("/ScoDoc/toggle_dept_vis/<int:dept_id>", methods=["GET", "POST"])
@admin_required @admin_required
def toggle_dept_vis(dept_id): def toggle_dept_vis(dept_id):
"""Cache ou rend visible un dept""" """Cache ou rend visible un dept"""

View File

@ -0,0 +1,41 @@
"""module_type_non_null
Revision ID: b9aadc10227f
Revises: bd2c1c3d866e
Create Date: 2022-02-15 21:47:29.212329
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "b9aadc10227f"
down_revision = "bd2c1c3d866e"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
# Added by ev: remove duplicates
bind = op.get_bind()
session = Session(bind=bind)
session.execute(
"""UPDATE notes_modules SET module_type=0 WHERE module_type IS NULL;"""
)
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"notes_modules", "module_type", existing_type=sa.INTEGER(), nullable=False
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"notes_modules", "module_type", existing_type=sa.INTEGER(), nullable=True
)
# ### end Alembic commands ###

View File

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

View File

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

View File

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

View File

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