diff --git a/app/__init__.py b/app/__init__.py
index cedb4564..a976da7f 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -296,10 +296,12 @@ def create_app(config_class=DevConfig):
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
+ from app.but.bulletin_but_pdf import BulletinGeneratorStandardBUT
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
- # l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
+ # l'ordre est important, le premier sera le "défaut" pour les nouveaux départements.
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
+ sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandardBUT)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
if app.testing or app.debug:
diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 771746bf..87ec62a2 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -11,8 +11,8 @@ import datetime
from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT
-from app.models import FormSemestre, Identite
-from app.scodoc import sco_utils as scu
+from app.models import FormSemestre, Identite, formsemestre
+from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_preferences
@@ -217,7 +217,7 @@ class BulletinBUT:
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
def bulletin_etud(
- self, etud: Identite, formsemestre, force_publishing=False
+ self, etud: Identite, formsemestre: FormSemestre, force_publishing=False
) -> dict:
"""Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
@@ -317,11 +317,29 @@ class BulletinBUT:
return d
- def bulletin_etud_complet(self, etud) -> dict:
+ def bulletin_etud_complet(self, etud: Identite) -> dict:
"""Bulletin dict complet avec toutes les infos pour les bulletins pdf"""
- d = self.bulletin_etud(force_publishing=True)
+ d = self.bulletin_etud(etud, self.res.formsemestre, force_publishing=True)
+ d["etudid"] = etud.id
+ d["etud"] = d["etudiant"]
+ d["etud"]["nomprenom"] = etud.nomprenom
+ d.update(self.res.sem)
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
- self.res.get_etud_etat(etud.id), self.prefs
+ self.res.get_etud_etat(etud.id),
+ self.prefs,
+ decision_sem=d["semestre"].get("decision_sem"),
)
- # XXX TODO A COMPLETER
- raise NotImplementedError()
+ # --- Absences
+ d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
+ # --- Rangs
+ d[
+ "rang_nt"
+ ] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
+ d["rang_txt"] = "Rang " + d["rang_nt"]
+
+ # --- Appréciations
+ d.update(
+ sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
+ )
+ # XXX TODO A COMPLETER ?
+ return d
diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py
new file mode 100644
index 00000000..5003e216
--- /dev/null
+++ b/app/but/bulletin_but_pdf.py
@@ -0,0 +1,116 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Génération bulletin BUT au format PDF standard
+"""
+
+import datetime
+from app.scodoc.sco_pdf import blue, cm, mm
+
+from flask import url_for, g
+from app.models.formsemestre import FormSemestre
+
+from app.scodoc import gen_tables
+from app.scodoc import sco_utils as scu
+from app.scodoc import sco_bulletins_json
+from app.scodoc import sco_preferences
+from app.scodoc.sco_codes_parcours import UE_SPORT
+from app.scodoc.sco_utils import fmt_note
+from app.comp.res_but import ResultatsSemestreBUT
+
+from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
+
+
+class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
+ """Génération du bulletin de BUT au format PDF.
+
+ self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
+ """
+
+ list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
+
+ def bul_table(self, format="html"):
+ """Génère la table centrale du bulletin de notes
+ Renvoie:
+ - en HTML: une chaine
+ - en PDF: une liste d'objets PLATYPUS (eg instance de Table).
+ """
+ formsemestre_id = self.infos["formsemestre_id"]
+ (
+ synth_col_keys,
+ synth_P,
+ synth_pdf_style,
+ synth_col_widths,
+ ) = self.but_table_synthese()
+ #
+ table_synthese = gen_tables.GenTable(
+ rows=synth_P,
+ columns_ids=synth_col_keys,
+ pdf_table_style=synth_pdf_style,
+ pdf_col_widths=[synth_col_widths[k] for k in synth_col_keys],
+ preferences=self.preferences,
+ html_class="notes_bulletin",
+ html_class_ignore_default=True,
+ html_with_td_classes=True,
+ )
+ # Ici on ajoutera table des ressources, tables des UE
+ # TODO
+
+ # XXX à modifier pour générer plusieurs tables:
+ return table_synthese.gen(format=format)
+
+ def but_table_synthese(self):
+ """La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
+ et leurs coefs.
+ Renvoie: colkeys, P, pdf_style, colWidths
+ - colkeys: nom des colonnes de la table (clés)
+ - P : table (liste de dicts de chaines de caracteres)
+ - pdf_style : commandes table Platypus
+ - largeurs de colonnes pour PDF
+ """
+ col_widths = {
+ "titre": None,
+ "moyenne": 2 * cm,
+ "coef": 2 * cm,
+ }
+ P = [] # elems pour générer table avec gen_table (liste de dicts)
+ col_keys = ["titre", "moyenne"] # noms des colonnes à afficher
+ for ue_acronym, ue in self.infos["ues"].items():
+ # 1er ligne titre UE
+ moy_ue = ue.get("moyenne")
+ t = {
+ "titre": f"{ue_acronym} - {ue['titre']}",
+ "moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-",
+ "_css_row_class": "note_bold",
+ "_pdf_row_markup": ["b"],
+ "_pdf_style": [],
+ }
+ P.append(t)
+ # 2eme ligne titre UE (bonus/malus/ects)
+ t = {
+ "titre": "",
+ "moyenne": f"""Bonus: {ue['bonus']} - Malus: {
+ ue["malus"]} - ECTS: {ue["ECTS"]["acquis"]} / {ue["ECTS"]["total"]}""",
+ "_css_row_class": "note_bold",
+ "_pdf_row_markup": ["b"],
+ "_pdf_style": [
+ (
+ "LINEBELOW",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ self.PDF_LINECOLOR,
+ )
+ ],
+ }
+ P.append(t)
+
+ # Global pdf style commands:
+ pdf_style = [
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
+ ("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
+ ]
+ return col_keys, P, pdf_style, col_widths
diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index 1c7fc543..0a95621e 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -13,6 +13,7 @@ Les classes de Bonus fournissent deux méthodes:
"""
import datetime
+import math
import numpy as np
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).
"""
- # 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:
@@ -106,6 +107,8 @@ class BonusSport:
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
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:
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.
Arguments:
- 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.
- modimpl_coefs_etuds_no_nan:
les coefficients: float ndarray
@@ -201,7 +205,8 @@ class BonusSportAdditif(BonusSport):
"""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...
@@ -224,22 +229,28 @@ class BonusSportAdditif(BonusSport):
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)
- 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(
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):
"""Bonus sport qui multiplie les moyennes d'UE par un facteur"""
@@ -284,6 +295,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,8 +306,68 @@ class BonusDirect(BonusSportAdditif):
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 :
+# 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):
- """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.
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):
- """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
sport , etc) non rattachés à une unité d'enseignement. Les points
@@ -330,28 +402,91 @@ class BonusBezier(BonusSportAdditif):
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.
-
+
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 +class BonusCachan1(BonusSportAdditif): + """Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1. + +À 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" @@ -411,11 +548,13 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif): 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. - 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). +Les points au-dessus de 10 sur 20 obtenus dans chacune des matières optionnelles sont cumulés. +
+Dans tous les cas, le bonus est dans la limite de 0,5 point.
""" name = "bonus_iutlemans" @@ -471,14 +611,15 @@ class BonusLeMans(BonusSportAdditif): # Bonus simple, mais avec changement de paramètres en 2010 ! 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 +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" @@ -530,17 +671,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" @@ -561,11 +704,11 @@ 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 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 de l'Université Paris 13 (sports, musique, deuxième langue, @@ -584,13 +727,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" @@ -611,15 +755,17 @@ class BonusTours(BonusDirect): 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 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. +Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par + l'étudiant.
""" name = "bonus_iutva" @@ -645,7 +791,7 @@ class BonusVilleAvray(BonusSport): 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 de l'Université Paris 13 (sports, musique, deuxième langue, @@ -657,7 +803,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..e5ba903c --- /dev/null +++ b/app/comp/moy_mat.py @@ -0,0 +1,52 @@ +############################################################################## +# 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_gen, _, _ = moy_ue.compute_ue_moys_classic( + formsemestre, + sem_matrix=sem_matrix, + ues=ues, + modimpl_inscr_df=modimpl_inscr_df, + modimpl_coefs=modimpl_coefs, + modimpl_mask=modimpl_mask, + ) + matiere_moy[matiere_id] = etud_moy_gen + 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_ue.py b/app/comp/moy_ue.py index b0a534e5..563fb3b1 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) @@ -266,6 +270,8 @@ def compute_ue_moys_apc( ) # Annule les coefs des modules NaN 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 # @@ -348,7 +354,8 @@ def compute_ue_moys_classic( 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) # --------------------- Calcul des moyennes d'UE ue_modules = np.array( [[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues] @@ -358,6 +365,8 @@ def compute_ue_moys_classic( ) # 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) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) etud_moy_ue = ( np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index b74efb10..1d01f4f4 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -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,6 +69,7 @@ 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( @@ -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( diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 2caf515c..ecc1e500 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,30 @@ 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 + + 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 self.apply_capitalisation() # --- 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 +163,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..5f652ec5 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -39,6 +39,7 @@ class ResultatsSemestre(ResultatsCache): "modimpl_inscr_df", "modimpls_results", "etud_coef_ue_df", + "moyennes_matieres", ) def __init__(self, formsemestre: FormSemestre): @@ -57,6 +58,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 +168,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,7 +186,9 @@ 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"]): @@ -195,6 +199,12 @@ 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): """""" @@ -510,8 +520,9 @@ class NotesTableCompat(ResultatsSemestre): def get_etud_mat_moy(self, matiere_id, etudid): """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][etudid] def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl 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 2d491d7e..1ae0a5a8 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) @@ -356,7 +361,7 @@ class FormSemestre(db.Model): def get_abs_count(self, etudid): """Les comptes d'absences de cet étudiant dans ce semestre: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs, nb abs justifiées) Utilise un cache. """ from app.scodoc import sco_abs 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/modules.py b/app/models/modules.py index 393cc8c0..5a5f4761 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -34,7 +34,7 @@ class Module(db.Model): # id de l'element pedagogique Apogee correspondant: code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) # 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: modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True) diff --git a/app/scodoc/sco_abs.py b/app/scodoc/sco_abs.py index 73345296..071cbe8e 100644 --- a/app/scodoc/sco_abs.py +++ b/app/scodoc/sco_abs.py @@ -1037,7 +1037,7 @@ def get_abs_count(etudid, sem): def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: - tuple (nb abs non justifiées, nb abs justifiées) + tuple (nb abs, nb abs justifiées) Utilise un cache. """ key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 64284105..ba1c1b44 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -65,7 +65,7 @@ from app.scodoc import sco_preferences from app.scodoc import sco_pvjury from app.scodoc import sco_users import app.scodoc.sco_utils as scu -from app.scodoc.sco_utils import ModuleType +from app.scodoc.sco_utils import ModuleType, fmt_note import app.scodoc.notesdb as ndb # ----- CLASSES DE BULLETINS DE NOTES @@ -189,7 +189,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): formsemestre.etuds_inscriptions[etudid].etat ) I["etud_etat"] = nt.get_etud_etat(etudid) - I["filigranne"] = sco_bulletins_pdf.get_filigranne(I["etud_etat"], prefs) + I["filigranne"] = sco_bulletins_pdf.get_filigranne( + I["etud_etat"], prefs, decision_dem=I["decision_sem"] + ) I["demission"] = "" if I["etud_etat"] == scu.DEMISSION: I["demission"] = "(Démission)" @@ -197,15 +199,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["demission"] = "(Défaillant)" # --- Appreciations - cnx = ndb.GetDBConnexion() - apprecs = sco_etud.appreciations_list( - cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} - ) - I["appreciations_list"] = apprecs - I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs] - I["appreciations"] = I[ - "appreciations_txt" - ] # deprecated / keep it for backward compat in templates + I.update(get_appreciations_list(formsemestre_id, etudid)) # --- Notes ues = nt.get_ues_stat_dict() @@ -297,7 +291,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): else: u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs" else: - u["cur_moy_ue_txt"] = "bonus de %.3g points" % x + u["cur_moy_ue_txt"] = f"bonus de {fmt_note(x)} points" + 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"]) @@ -395,6 +391,21 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): return C +def get_appreciations_list(formsemestre_id: int, etudid: int) -> dict: + """Appréciations pour cet étudiant dans ce semestre""" + cnx = ndb.GetDBConnexion() + apprecs = sco_etud.appreciations_list( + cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} + ) + d = { + "appreciations_list": apprecs, + "appreciations_txt": [x["date"] + ": " + x["comment"] for x in apprecs], + } + # deprecated / keep it for backward compat in templates: + d["appreciations"] = d["appreciations_txt"] + return d + + def _get_etud_etat_html(etat: str) -> str: """chaine html représentant l'état (backward compat sco7)""" if etat == scu.INSCRIT: # "I" @@ -921,7 +932,7 @@ def do_formsemestre_bulletinetud( if formsemestre.formation.is_apc(): etud = Identite.query.get(etudid) r = bulletin_but.BulletinBUT(formsemestre) - I = r.bulletin_etud_complet(etud, formsemestre) + I = r.bulletin_etud_complet(etud) else: I = formsemestre_bulletinetud_dict(formsemestre.id, etudid) etud = I["etud"] diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py index aafdc09f..ceeb0aac 100644 --- a/app/scodoc/sco_bulletins_generator.py +++ b/app/scodoc/sco_bulletins_generator.py @@ -63,41 +63,6 @@ from app.scodoc import sco_pdf from app.scodoc.sco_pdf import PDFLOCK import sco_version -# Liste des types des classes de générateurs de bulletins PDF: -BULLETIN_CLASSES = collections.OrderedDict() - - -def register_bulletin_class(klass): - BULLETIN_CLASSES[klass.__name__] = klass - - -def bulletin_class_descriptions(): - return [x.description for x in BULLETIN_CLASSES.values()] - - -def bulletin_class_names(): - return list(BULLETIN_CLASSES.keys()) - - -def bulletin_default_class_name(): - return bulletin_class_names()[0] - - -def bulletin_get_class(class_name): - return BULLETIN_CLASSES[class_name] - - -def bulletin_get_class_name_displayed(formsemestre_id): - """Le nom du générateur utilisé, en clair""" - from app.scodoc import sco_preferences - - bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) - try: - gen_class = bulletin_get_class(bul_class_name) - return gen_class.description - except: - return "invalide ! (voir paramètres)" - class BulletinGenerator: "Virtual superclass for PDF bulletin generators" "" @@ -105,6 +70,7 @@ class BulletinGenerator: # see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ] description = "superclass for bulletins" # description for user interface + list_in_menu = True # la classe doit-elle est montrée dans le menu de config ? def __init__( self, @@ -270,9 +236,14 @@ def make_formsemestre_bulletinetud( formsemestre_id = infos["formsemestre_id"] bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) - try: + + gen_class = None + if infos.get("type") == "BUT" and format.startswith("pdf"): + gen_class = bulletin_get_class(bul_class_name + "BUT") + if gen_class is None: gen_class = bulletin_get_class(bul_class_name) - except: + + if gen_class is None: raise ValueError( "Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name ) @@ -313,3 +284,48 @@ def make_formsemestre_bulletinetud( filename = bul_generator.get_filename() return data, filename + + +#### + +# Liste des types des classes de générateurs de bulletins PDF: +BULLETIN_CLASSES = collections.OrderedDict() + + +def register_bulletin_class(klass): + BULLETIN_CLASSES[klass.__name__] = klass + + +def bulletin_class_descriptions(): + return [x.description for x in BULLETIN_CLASSES.values()] + + +def bulletin_class_names() -> list[str]: + "Liste les noms des classes de bulletins à présenter à l'utilisateur" + return [ + class_name + for class_name in BULLETIN_CLASSES + if BULLETIN_CLASSES[class_name].list_in_menu + ] + + +def bulletin_default_class_name(): + return bulletin_class_names()[0] + + +def bulletin_get_class(class_name: str) -> BulletinGenerator: + """La class de génération de bulletin de ce nom, + ou None si pas trouvée + """ + return BULLETIN_CLASSES.get(class_name) + + +def bulletin_get_class_name_displayed(formsemestre_id): + """Le nom du générateur utilisé, en clair""" + from app.scodoc import sco_preferences + + bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) + gen_class = bulletin_get_class(bul_class_name) + if gen_class is None: + return "invalide ! (voir paramètres)" + return gen_class.description diff --git a/app/scodoc/sco_bulletins_pdf.py b/app/scodoc/sco_bulletins_pdf.py index 748fd5a0..1df2ca66 100644 --- a/app/scodoc/sco_bulletins_pdf.py +++ b/app/scodoc/sco_bulletins_pdf.py @@ -276,13 +276,13 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"): return pdfdoc, filename -def get_filigranne(etud_etat: str, prefs) -> str: +def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str: """Texte à placer en "filigranne" sur le bulletin pdf""" if etud_etat == scu.DEMISSION: return "Démission" elif etud_etat == sco_codes_parcours.DEF: return "Défaillant" - elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[ + elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[ "bul_show_temporary_forced" ]: return prefs["bul_temporary_txt"] diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index 60c6f2a0..0485a756 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -66,7 +66,8 @@ from app.scodoc import sco_groups from app.scodoc import sco_evaluations from app.scodoc import gen_tables -# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences) +# Important: Le nom de la classe ne doit pas changer (bien le choisir), +# car il sera stocké en base de données (dans les préférences) class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc supported_formats = ["html", "pdf"] @@ -264,11 +265,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): def build_bulletin_table(self): """Génère la table centrale du bulletin de notes - Renvoie: colkeys, P, pdf_style, colWidths - - colkeys: nom des colonnes de la table (clés) - - table (liste de dicts de chaines de caracteres) - - style (commandes table Platypus) - - largeurs de colonnes pour PDF + Renvoie: col_keys, P, pdf_style, col_widths + - col_keys: nom des colonnes de la table (clés) + - table: liste de dicts de chaines de caractères + - pdf_style: commandes table Platypus + - col_widths: largeurs de colonnes pour PDF """ I = self.infos P = [] # elems pour générer table avec gen_table (liste de dicts) @@ -287,25 +288,25 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): with_col_coef = prefs["bul_show_coef"] with_col_ects = prefs["bul_show_ects"] - colkeys = ["titre", "module"] # noms des colonnes à afficher + col_keys = ["titre", "module"] # noms des colonnes à afficher if with_col_rang: - colkeys += ["rang"] + col_keys += ["rang"] if with_col_minmax: - colkeys += ["min"] + col_keys += ["min"] if with_col_moypromo: - colkeys += ["moy"] + col_keys += ["moy"] if with_col_minmax: - colkeys += ["max"] - colkeys += ["note"] + col_keys += ["max"] + col_keys += ["note"] if with_col_coef: - colkeys += ["coef"] + col_keys += ["coef"] if with_col_ects: - colkeys += ["ects"] + col_keys += ["ects"] if with_col_abs: - colkeys += ["abs"] + col_keys += ["abs"] colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus) i = 0 - for k in colkeys: + for k in col_keys: colidx[k] = i i += 1 @@ -313,7 +314,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm else: bul_pdf_mod_colwidth = None - colWidths = { + col_widths = { "titre": None, "module": bul_pdf_mod_colwidth, "min": 1.5 * cm, @@ -541,7 +542,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): ("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu: ] # - return colkeys, P, pdf_style, colWidths + return col_keys, P, pdf_style, col_widths def _list_modules( self, diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 41a25d09..9c8aa07d 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -500,13 +500,20 @@ 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 ] else: mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] - ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] + ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) semestres_indices = list(range(1, parcours.NB_SEM + 1)) @@ -734,8 +741,11 @@ def module_edit(module_id=None): else: # l'UE de rattachement peut changer tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") + x, y = tf[2]["ue_matiere_id"].split("!") + tf[2]["ue_id"] = int(x) + tf[2]["matiere_id"] = int(y) old_ue_id = a_module.ue.id - new_ue_id = int(tf[2]["ue_id"]) + new_ue_id = tf[2]["ue_id"] if (old_ue_id != new_ue_id) and in_use: new_ue = UniteEns.query.get_or_404(new_ue_id) if new_ue.semestre_idx != a_module.ue.semestre_idx: diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 17cdc8c0..40810912 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -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) 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 +669,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( - """