From 1dbb199d2cde9d577b481e0c177322dfead20dd3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 17 Feb 2022 18:13:04 +0100 Subject: [PATCH 01/74] Ajout relations pour acces aux partitions et groupes via l'ORM --- app/models/formsemestre.py | 5 +++++ app/models/groups.py | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 245142484..a6411f70f 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) diff --git a/app/models/groups.py b/app/models/groups.py index 902298ccf..976d465be 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", From 98e7f7a7105bbadf78cd4313b2969beb3b2f3337 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 17 Feb 2022 22:40:11 +0100 Subject: [PATCH 02/74] Corrige affichage numero version sur templates Jinja --- app/templates/sidebar.html | 2 +- app/views/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/templates/sidebar.html b/app/templates/sidebar.html index c5b06a6f3..5ed8ee883 100644 --- a/app/templates/sidebar.html +++ b/app/templates/sidebar.html @@ -4,7 +4,7 @@ From cca72dfed27089f9308cf3d6ce13df994110a4cc Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 19 Feb 2022 00:28:24 +0100 Subject: [PATCH 21/74] Bonus IUT Amiens --- app/comp/bonus_spo.py | 15 +++++++++++++++ sco_version.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index ed4bf5db3..65f6c03b9 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -295,6 +295,21 @@ 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. diff --git a/sco_version.py b/sco_version.py index 04070622d..96ef7d1da 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.59" +SCOVERSION = "9.1.60" SCONAME = "ScoDoc" From 63784e341ad1d33f1bdb7060b88e25e0b3ad4d9e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 19 Feb 2022 01:00:40 +0100 Subject: [PATCH 22/74] Correction pour Amiens, Roanne --- app/comp/bonus_spo.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 65f6c03b9..0a95621e8 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -205,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... @@ -228,12 +229,22 @@ 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( @@ -693,7 +704,7 @@ 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): @@ -792,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): From 44123c022eac799449173f8d92734bcebb75f8c1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 19 Feb 2022 16:16:52 +0100 Subject: [PATCH 23/74] =?UTF-8?q?Am=C3=A9liore=20=C3=A9dition=20programmes?= =?UTF-8?q?=20classiques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_edit_ue.py | 34 +++++++++++++++++------ app/scodoc/sco_formsemestre_validation.py | 16 +++++++---- app/static/css/scodoc.css | 4 +++ sco_version.py | 2 +- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 17cdc8c07..061bf43c6 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( - """
Attention: plusieurs UE de cette - formation ont le même code. Il faut corriger cela ci-dessous, - sinon les calculs d'ECTS seront erronés !
""" + f"""
Attention: plusieurs UE de cette + formation ont le même code : { + ', '.join([ + '' + ue["acronyme"] + " (code " + ue["ue_code"] + ")" + for ue in ues_with_duplicated_code ]) + }. + Il faut corriger cela, sinon les capitalisations et ECTS seront + erronés !
""" ) # Description de la formation @@ -930,8 +941,8 @@ def _ue_table_ues( if cur_ue_semestre_id != ue["semestre_id"]: cur_ue_semestre_id = ue["semestre_id"] - if iue > 0: - H.append("") + # if iue > 0: + # H.append("") if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: lab = "Pas d'indication de semestre:" else: @@ -953,7 +964,6 @@ def _ue_table_ues( ) else: H.append(arrow_none) - iue += 1 ue["acro_titre"] = str(ue["acronyme"]) if ue["titre"] != ue["acronyme"]: ue["acro_titre"] += " " + str(ue["titre"]) @@ -1001,6 +1011,14 @@ def _ue_table_ues( delete_disabled_icon, ) ) + if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]: + H.append( + f"""""" + ) + iue += 1 + return "\n".join(H) diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index f755bb118..987dc962f 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -1250,7 +1250,7 @@ def check_formation_ues(formation_id): for ue in ues: # formsemestres utilisant cette ue ? sems = ndb.SimpleDictFetch( - """SELECT DISTINCT sem.id AS formsemestre_id, sem.* + """SELECT DISTINCT sem.id AS formsemestre_id, sem.* FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi WHERE sem.formation_id = %(formation_id)s AND mod.id = mi.module_id @@ -1269,11 +1269,11 @@ def check_formation_ues(formation_id): return "", {} # Genere message HTML: H = [ - """
Attention: les UE suivantes de cette formation + """
Attention: les UE suivantes de cette formation sont utilisées dans des - semestres de rangs différents (eg S1 et S3).
Cela peut engendrer des problèmes pour - la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation: - soit modifier le programme de la formation (définir des UE dans chaque semestre), + semestres de rangs différents (eg S1 et S3).
Cela peut engendrer des problèmes pour + la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation: + soit modifier le programme de la formation (définir des UE dans chaque semestre), soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une UE extérieure.
    @@ -1286,7 +1286,11 @@ def check_formation_ues(formation_id): for x in ue_multiples[ue["ue_id"]] ] slist = ", ".join( - ["%(titreannee)s (semestre %(semestre_id)s)" % s for s in sems] + [ + """%(titreannee)s (semestre %(semestre_id)s)""" + % s + for s in sems + ] ) H.append("
  • %s : %s
  • " % (ue["acronyme"], slist)) H.append("
") diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index e88930857..ecd8b0ab0 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1707,6 +1707,9 @@ ul.notes_ue_list { li.notes_ue_list { margin-top: 9px; list-style-type: none; + border: 1px solid maroon; + border-radius: 10px; + padding-bottom: 5px; } span.ue_type_1 { color: green; @@ -1749,6 +1752,7 @@ ul.notes_matiere_list { background-color: rgb(220,220,220); font-weight: normal; font-style: italic; + border-top: 1px solid maroon; } ul.notes_module_list { diff --git a/sco_version.py b/sco_version.py index 96ef7d1da..9e02cffd1 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.60" +SCOVERSION = "9.1.61" SCONAME = "ScoDoc" From ba974df04f138743168b732f2f254852b6985ac6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Feb 2022 15:10:10 +0100 Subject: [PATCH 24/74] =?UTF-8?q?Fix:=20mise=20=C3=A0=20jour=20bonus=20sur?= =?UTF-8?q?=20oy.=20gen.=20apr=C3=A8s=20capitalisation=20UEs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_common.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index b019c9770..14042364a 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -165,7 +165,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 +183,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 +196,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): """""" From 0801919b80d6e24c81f8dab3b94489c74c53f5bc Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Feb 2022 17:36:50 +0100 Subject: [PATCH 25/74] =?UTF-8?q?Calcul=20moyennes=20mati=C3=A8res=20(form?= =?UTF-8?q?ations=20classiques).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mat.py | 52 +++++++++++++++++++++++++++++++++++++++++ app/comp/moy_ue.py | 1 - app/comp/res_classic.py | 17 +++++++++++++- app/comp/res_common.py | 8 +++++-- 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 app/comp/moy_mat.py diff --git a/app/comp/moy_mat.py b/app/comp/moy_mat.py new file mode 100644 index 000000000..e5ba903c2 --- /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_ue.py b/app/comp/moy_ue.py index 8b98d2ef4..563fb3b1d 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 diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 1dfcfb4da..ecc1e5004 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 @@ -133,6 +134,10 @@ class ResultatsSemestreClassic(NotesTableCompat): # --- 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) @@ -158,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 14042364a..5f652ec5c 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" @@ -517,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 From d314d47dc5044f351a53f378792dc7cfb8ccf037 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 21 Feb 2022 18:51:45 +0100 Subject: [PATCH 26/74] traitement erreur sur formsemestre_description --- app/scodoc/sco_formsemestre_status.py | 7 ++++--- sco_version.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index d8be56f27..2c8465150 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -595,11 +595,12 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): """Description du semestre sous forme de table exportable Liste des modules et de leurs coefficients """ - sem = sco_formsemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) - F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] + F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[ + 0 + ] parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) Mlist = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id, sort_by_ue=True @@ -709,7 +710,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False): titles["coefficient"] = "Coef. éval." titles["evalcomplete_str"] = "Complète" titles["publish_incomplete_str"] = "Toujours Utilisée" - title = "%s %s" % (parcours.SESSION_NAME.capitalize(), sem["titremois"]) + title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois()) return GenTable( columns_ids=columns_ids, diff --git a/sco_version.py b/sco_version.py index 9e02cffd1..4ac94b210 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.61" +SCOVERSION = "9.1.62" SCONAME = "ScoDoc" From 0e42df55c927bde01949984d4c20a741db03e834 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 22 Feb 2022 18:44:53 +0100 Subject: [PATCH 27/74] =?UTF-8?q?Option=20pour=20afficher=20coef.=20UE=20s?= =?UTF-8?q?=C3=A9par=C3=A9e=20de=20celle=20pour=20les=20coefs=20modules=20?= =?UTF-8?q?(et=20=C3=A9vals).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_bulletins_standard.py | 10 ++++++---- app/scodoc/sco_preferences.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py index 60c6f2a00..25111f7e6 100644 --- a/app/scodoc/sco_bulletins_standard.py +++ b/app/scodoc/sco_bulletins_standard.py @@ -284,7 +284,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): ) with_col_moypromo = prefs["bul_show_moypromo"] with_col_rang = prefs["bul_show_rangs"] - with_col_coef = prefs["bul_show_coef"] + with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"] with_col_ects = prefs["bul_show_ects"] colkeys = ["titre", "module"] # noms des colonnes à afficher @@ -409,7 +409,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): # Chaque UE: for ue in I["ues"]: ue_type = None - coef_ue = ue["coef_ue_txt"] + coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else "" ue_descr = ue["ue_descr_txt"] rowstyle = "" plusminus = minuslink # @@ -592,7 +592,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): "_titre_colspan": 2, "rang": mod["mod_rang_txt"], # vide si pas option rang "note": mod["mod_moy_txt"], - "coef": mod["mod_coef_txt"], + "coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "", "abs": mod.get( "mod_abs_txt", "" ), # absent si pas option show abs module @@ -656,7 +656,9 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): eval_style = "" t = { "module": ' ' + e["name"], - "coef": "" + e["coef_txt"] + "", + "coef": ("" + e["coef_txt"] + "") + if prefs["bul_show_coef"] + else "", "_hidden": hidden, "_module_target": e["target_html"], # '_module_help' : , diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index b639d5ee4..aab148e70 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -1296,11 +1296,21 @@ class BasePreferences(object): "labels": ["non", "oui"], }, ), + ( + "bul_show_ue_coef", + { + "initvalue": 1, + "title": "Afficher coefficient des UE sur les bulletins", + "input_type": "boolcheckbox", + "category": "bul", + "labels": ["non", "oui"], + }, + ), ( "bul_show_coef", { "initvalue": 1, - "title": "Afficher coefficient des ue/modules sur les bulletins", + "title": "Afficher coefficient des modules sur les bulletins", "input_type": "boolcheckbox", "category": "bul", "labels": ["non", "oui"], From 276d7977a79e9167df9a66d1b8d6f7bfa3f0460c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 22 Feb 2022 18:45:43 +0100 Subject: [PATCH 28/74] Ajout UE bonus aux parcours ILEPS --- app/scodoc/sco_codes_parcours.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 59372b8d6..f6ca2ff29 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -587,7 +587,7 @@ class ParcoursILEPS(TypeParcours): # SESSION_ABBRV = 'A' # A1, A2, ... COMPENSATION_UE = False UNUSED_CODES = set((ADC, ATT, ATB, ATJ)) - ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE] + ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE, UE_SPORT] # Barre moy gen. pour validation semestre: BARRE_MOY = 10.0 # Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales") From e7b980bff7d3be0a9021091509a65c7bcba905e6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 22 Feb 2022 18:46:47 +0100 Subject: [PATCH 29/74] =?UTF-8?q?Fix:=20acc=C3=A8s=20moyennes=5Fmatieres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 5f652ec5c..92b0ee5ff 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -518,11 +518,11 @@ class NotesTableCompat(ResultatsSemestre): return "" return ins.etat - def get_etud_mat_moy(self, matiere_id, etudid): + def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str: """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" if not self.moyennes_matieres: return "nd" - return self.moyennes_matieres[matiere_id][etudid] + return self.moyennes_matieres[matiere_id].get(etudid, "-") def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl From 875c12d703ea2e888d0cc09591caeed3ff15754b Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Tue, 22 Feb 2022 19:25:49 +0100 Subject: [PATCH 30/74] soften error when logo not found --- app/scodoc/sco_logos.py | 2 +- app/views/scodoc.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index c489510b5..46e0c066a 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -276,7 +276,7 @@ class Logo: if self.mm is None: return f'' else: - return f'' + return f'' def last_modified(self): path = Path(self.filepath) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 809d8e463..d1c842dd4 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -304,8 +304,9 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True): # stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici # from app.scodoc.sco_photos import _http_jpeg_file - logo = sco_logos.find_logo(name, dept_id, strict).select() + logo = sco_logos.find_logo(name, dept_id, strict) if logo is not None: + logo.select() suffix = logo.suffix if small: with PILImage.open(logo.filepath) as im: From e9ad417f1f5abcc9eec5390959b2df005cc84dc9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 23 Feb 2022 09:42:41 +0100 Subject: [PATCH 31/74] check matieres --- app/comp/res_common.py | 6 +++++- sco_version.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 92b0ee5ff..f13201262 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -522,7 +522,11 @@ class NotesTableCompat(ResultatsSemestre): """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" if not self.moyennes_matieres: return "nd" - return self.moyennes_matieres[matiere_id].get(etudid, "-") + return ( + self.moyennes_matieres[matiere_id].get(etudid, "-") + if matiere_id in self.moyennes_matieres + else "-" + ) def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl diff --git a/sco_version.py b/sco_version.py index 4ac94b210..5c1b87f2e 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.62" +SCOVERSION = "9.1.63" SCONAME = "ScoDoc" From 2cac0031f6c4aa03cf8bbf03d5989b509dc819d8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 23 Feb 2022 20:15:28 +0100 Subject: [PATCH 32/74] Erreur si la reponse portail n'a pas le mail --- app/scodoc/sco_synchro_etuds.py | 38 ++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index bbcf4a083..db775438c 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -854,23 +854,27 @@ def formsemestre_import_etud_admission( apo_emailperso = etud.get("mailperso", "") if info["emailperso"] and not apo_emailperso: apo_emailperso = info["emailperso"] - if ( - import_email - and info["email"] != etud["mail"] - or info["emailperso"] != apo_emailperso - ): - sco_etud.adresse_edit( - cnx, - args={ - "etudid": etudid, - "adresse_id": info["adresse_id"], - "email": etud["mail"], - "emailperso": apo_emailperso, - }, - ) - # notifie seulement les changements d'adresse mail institutionnelle - if info["email"] != etud["mail"]: - changed_mails.append((info, etud["mail"])) + if import_email: + if not "mail" in etud: + raise ScoValueError( + "la réponse portail n'a pas le champs requis 'mail'" + ) + if ( + info["email"] != etud["mail"] + or info["emailperso"] != apo_emailperso + ): + sco_etud.adresse_edit( + cnx, + args={ + "etudid": etudid, + "adresse_id": info["adresse_id"], + "email": etud["mail"], + "emailperso": apo_emailperso, + }, + ) + # notifie seulement les changements d'adresse mail institutionnelle + if info["email"] != etud["mail"]: + changed_mails.append((info, etud["mail"])) else: unknowns.append(code_nip) sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"]) From 6a07bb85a0e3b93350b1eebcd5a649a7fae74bc7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 23 Feb 2022 20:21:13 +0100 Subject: [PATCH 33/74] Message erreur si bul_intro_mail invalide --- app/scodoc/sco_bulletins.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index de7f28c9d..ca6748de7 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -1013,11 +1013,16 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr): intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id) if intro_mail: - hea = intro_mail % { - "nomprenom": etud["nomprenom"], - "dept": dept, - "webmaster": webmaster, - } + try: + hea = intro_mail % { + "nomprenom": etud["nomprenom"], + "dept": dept, + "webmaster": webmaster, + } + except KeyError as e: + raise ScoValueError( + "format 'Message d'accompagnement' (bul_intro_mail) invalide, revoir les réglages dans les préférences" + ) else: hea = "" From 9bc5f27b165395b7f7a32e977dbe0700d694c089 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 25 Feb 2022 10:30:57 +0100 Subject: [PATCH 34/74] moduleimpl_withmodule_list (api ScoDoc 7 compat): fix --- app/views/notes.py | 2 +- sco_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/notes.py b/app/views/notes.py index 7be106444..cd211ca05 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2686,7 +2686,7 @@ def moduleimpl_list( @permission_required(Permission.ScoView) @scodoc7func def moduleimpl_withmodule_list( - moduleimpl_id=None, formsemestre_id=None, module_id=None + moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json" ): """API ScoDoc 7""" data = sco_moduleimpl.moduleimpl_withmodule_list( diff --git a/sco_version.py b/sco_version.py index 5c1b87f2e..1bdc8323b 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.63" +SCOVERSION = "9.1.64" SCONAME = "ScoDoc" From aa609aa0cf44a800ae6ad5aa93bf8c47652947df Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 26 Feb 2022 10:09:14 +0100 Subject: [PATCH 35/74] =?UTF-8?q?Am=C3=A9liore=20form.=20logos=20(validati?= =?UTF-8?q?on=20des=20noms)=20+=20messages=20flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/forms/main/config_logos.py | 34 +++++++++++++++++++--------------- app/scodoc/sco_utils.py | 12 +++++++++++- app/templates/base.html | 10 ++++------ app/templates/sco_page.html | 10 ++++------ 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py index 2be78713d..c89983271 100644 --- a/app/forms/main/config_logos.py +++ b/app/forms/main/config_logos.py @@ -30,17 +30,15 @@ Formulaires configuration logos Contrib @jmp, dec 21 """ -import re from flask import flash, url_for, redirect, render_template from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed -from wtforms import SelectField, SubmitField, FormField, validators, FieldList +from wtforms import SubmitField, FormField, validators, FieldList +from wtforms import ValidationError from wtforms.fields.simple import StringField, HiddenField -from app import AccessDenied from app.models import Departement -from app.models import ScoDocSiteConfig from app.scodoc import sco_logos, html_sco_header from app.scodoc import sco_utils as scu from app.scodoc.sco_config_actions import ( @@ -49,10 +47,11 @@ from app.scodoc.sco_config_actions import ( LogoInsert, ) -from flask_login import current_user +from app.scodoc import sco_utils as scu from app.scodoc.sco_logos import find_logo + JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [] CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS @@ -111,6 +110,15 @@ def dept_key_to_id(dept_key): return dept_key +def logo_name_validator(message=None): + def validate_logo_name(form, field): + name = field.data if field.data else "" + if not scu.is_valid_filename(name): + raise ValidationError(message) + + return validate_logo_name + + class AddLogoForm(FlaskForm): """Formulaire permettant l'ajout d'un logo (dans un département)""" @@ -118,11 +126,7 @@ class AddLogoForm(FlaskForm): name = StringField( label="Nom", validators=[ - validators.regexp( - r"^[a-zA-Z0-9-_]*$", - re.IGNORECASE, - "Ne doit comporter que lettres, chiffres, _ ou -", - ), + logo_name_validator("Nom de logo invalide (alphanumérique, _)"), validators.Length( max=20, message="Un nom ne doit pas dépasser 20 caractères" ), @@ -373,11 +377,11 @@ def config_logos(): if action: action.execute() flash(action.message) - return redirect( - url_for( - "scodoc.configure_logos", - ) - ) + return redirect(url_for("scodoc.configure_logos")) + else: + if not form.validate(): + scu.flash_errors(form) + return render_template( "config_logos.html", scodoc_dept=None, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 1cc6e89c0..b3d99ac3e 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -50,7 +50,7 @@ import pydot import requests from flask import g, request -from flask import url_for, make_response, jsonify +from flask import flash, url_for, make_response, jsonify from config import Config from app import log @@ -616,6 +616,16 @@ def bul_filename(sem, etud, format): return filename +def flash_errors(form): + """Flashes form errors (version sommaire)""" + for field, errors in form.errors.items(): + flash( + "Erreur: voir le champs %s" % (getattr(form, field).label.text,), + "warning", + ) + # see https://getbootstrap.com/docs/4.0/components/alerts/ + + def sendCSVFile(data, filename): # DEPRECATED utiliser send_file """publication fichier CSV.""" return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True) diff --git a/app/templates/base.html b/app/templates/base.html index adf70171b..c4083a53c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -57,12 +57,10 @@ {% block content %}
- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} - - {% endfor %} - {% endif %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} + + {% endfor %} {% endwith %} {# application content needs to be provided in the app_content block #} diff --git a/app/templates/sco_page.html b/app/templates/sco_page.html index d3fbdcf87..e1aa9e3c5 100644 --- a/app/templates/sco_page.html +++ b/app/templates/sco_page.html @@ -23,12 +23,10 @@
- {% with messages = get_flashed_messages() %} - {% if messages %} - {% for message in messages %} - - {% endfor %} - {% endif %} + {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} + + {% endfor %} {% endwith %}
{% if sco.sem %} From 9b27503d019ce6a85ce6af748d7a82e300ec5437 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 26 Feb 2022 10:15:00 +0100 Subject: [PATCH 36/74] Fix: gestion logos --- app/scodoc/sco_logos.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/scodoc/sco_logos.py b/app/scodoc/sco_logos.py index 46e0c066a..02272ce87 100644 --- a/app/scodoc/sco_logos.py +++ b/app/scodoc/sco_logos.py @@ -151,6 +151,8 @@ class Logo: Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet """ self.logoname = secure_filename(logoname) + if not self.logoname: + self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***" self.scodoc_dept_id = dept_id self.prefix = prefix or "" if self.scodoc_dept_id: From dbab59039c7c2a8abe1e625027c8e7c622809d94 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 26 Feb 2022 11:00:08 +0100 Subject: [PATCH 37/74] Fix: recherche images fond de page (logos) --- app/scodoc/sco_pdf.py | 8 ++++++-- app/scodoc/sco_pvpdf.py | 6 ++++++ sco_version.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index 0e0996800..3949f50a9 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate): PageTemplate.__init__(self, "ScolarsPageTemplate", [content]) self.logo = None logo = find_logo( - logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None + logoname="bul_pdf_background", dept_id=g.scodoc_dept_id + ) or find_logo( + logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix="" ) if logo is None: # Also try to use PV background logo = find_logo( - logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None + logoname="letter_background", dept_id=g.scodoc_dept_id + ) or find_logo( + logoname="letter_background", dept_id=g.scodoc_dept_id, prefix="" ) if logo is not None: self.background_image_filename = logo.filepath diff --git a/app/scodoc/sco_pvpdf.py b/app/scodoc/sco_pvpdf.py index 1fb98e3d3..2e8cdaf66 100644 --- a/app/scodoc/sco_pvpdf.py +++ b/app/scodoc/sco_pvpdf.py @@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate): background = find_logo( logoname="pvjury_background", dept_id=g.scodoc_dept_id, + ) or find_logo( + logoname="pvjury_background", + dept_id=g.scodoc_dept_id, prefix="", ) else: background = find_logo( logoname="letter_background", dept_id=g.scodoc_dept_id, + ) or find_logo( + logoname="letter_background", + dept_id=g.scodoc_dept_id, prefix="", ) if not self.background_image_filename and background is not None: diff --git a/sco_version.py b/sco_version.py index 1bdc8323b..7eaec052d 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.64" +SCOVERSION = "9.1.65" SCONAME = "ScoDoc" From c1c9f22a319c5b3dfe0b0e0d4de5a3dcce1c4926 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 26 Feb 2022 20:11:22 +0100 Subject: [PATCH 38/74] exception -handling --- app/scodoc/sco_find_etud.py | 6 +++++- sco_version.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 157cb6e59..4bc039baa 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -39,6 +39,7 @@ from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_etud from app.scodoc import sco_groups +from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_permissions import Permission from app.scodoc import sco_preferences @@ -221,7 +222,10 @@ def search_etuds_infos(expnom=None, code_nip=None): cnx = ndb.GetDBConnexion() if expnom and not may_be_nip: expnom = expnom.upper() # les noms dans la BD sont en uppercase - etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~") + try: + etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~") + except ScoException: + etuds = [] else: code_nip = code_nip or expnom if code_nip: diff --git a/sco_version.py b/sco_version.py index 7eaec052d..b47a266d7 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.65" +SCOVERSION = "9.1.66" SCONAME = "ScoDoc" From c0494d8d71af1aa062d387ed0fe28319b60ace6a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 26 Feb 2022 20:22:18 +0100 Subject: [PATCH 39/74] exception handling (export Apo) --- app/scodoc/sco_apogee_csv.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index e7710a910..b1482ec5b 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -98,7 +98,7 @@ from chardet import detect as chardet_detect from app import log from app.comp import res_sem from app.comp.res_common import NotesTableCompat -from app.models import FormSemestre +from app.models import FormSemestre, Identite from app.models.config import ScoDocSiteConfig import app.scodoc.sco_utils as scu from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError @@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import ( NAR, RAT, ) -from app.scodoc import sco_cache from app.scodoc import sco_formsemestre from app.scodoc import sco_parcours_dut from app.scodoc import sco_etud @@ -454,6 +453,12 @@ class ApoEtud(dict): def comp_elt_semestre(self, nt, decision, etudid): """Calcul résultat apo semestre""" + if decision is None: + etud = Identite.query.get(etudid) + nomprenom = etud.nomprenom if etud else "(inconnu)" + raise ScoValueError( + f"decision absente pour l'étudiant {nomprenom} ({etudid})" + ) # resultat du semestre decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"]) note = nt.get_etud_moy_gen(etudid) From 40f823ee7c5f4cd453d44a7ecc363ca9eeb4325d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 26 Feb 2022 20:35:34 +0100 Subject: [PATCH 40/74] Fix: edition module --- app/scodoc/sco_edit_module.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 2bedc0c11..9ea4e2fe7 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -551,7 +551,11 @@ def module_edit(module_id=None): # ne propose pas SAE et Ressources, sauf si déjà de ce type... module_types = ( set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} - ) | {a_module.module_type or scu.ModuleType.STANDARD} + ) | { + scu.ModuleType(a_module.module_type) + if a_module.module_type + else scu.ModuleType.STANDARD + } descr = [ ( From 1dfccb67371ea0e0730acbb99603c934275e2b6c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Feb 2022 09:45:15 +0100 Subject: [PATCH 41/74] Modif bonus Roanne --- app/comp/bonus_spo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 0a95621e8..18c57beb0 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -705,6 +705,7 @@ class BonusRoanne(BonusSportAdditif): seuil_moy_gen = 0.0 bonus_max = 0.6 # plafonnement à 0.6 points classic_use_bonus_ues = True # sur les UE, même en DUT et LP + proportion_point = 1 class BonusStDenis(BonusSportAdditif): From 091d34dd8811db144a0343932e88f52dfc54604d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Feb 2022 10:19:25 +0100 Subject: [PATCH 42/74] =?UTF-8?q?Am=C3=A9liore=20creation=20UE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_edit_ue.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 061bf43c6..7124d3c30 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -29,7 +29,7 @@ """ import flask -from flask import url_for, render_template +from flask import flash, render_template, url_for from flask import g, request from flask_login import current_user @@ -107,8 +107,6 @@ def ue_list(*args, **kw): def do_ue_create(args): "create an ue" - from app.scodoc import sco_formations - cnx = ndb.GetDBConnexion() # check duplicates ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]}) @@ -117,6 +115,14 @@ def do_ue_create(args): f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! (chaque UE doit avoir un acronyme unique dans la formation)""" ) + if not "ue_code" in args: + # évite les conflits de code + while True: + cursor = db.session.execute("select notes_newid_ucod();") + code = cursor.fetchone()[0] + if UniteEns.query.filter_by(ue_code=code).count() == 0: + break + args["ue_code"] = code # create ue_id = _ueEditor.create(cnx, args) @@ -128,6 +134,8 @@ def do_ue_create(args): formation = Formation.query.get(args["formation_id"]) formation.invalidate_module_coefs() # news + ue = UniteEns.query.get(ue_id) + flash(f"UE créée (code {ue.ue_code})") formation = Formation.query.get(args["formation_id"]) sco_news.add( typ=sco_news.NEWS_FORM, From 6b8410e43b4ddecc712c4fbbe39a622e17340e89 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Feb 2022 17:49:39 +0100 Subject: [PATCH 43/74] cosmetic: edit prog. --- app/scodoc/sco_edit_ue.py | 3 ++- app/static/css/scodoc.css | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 7124d3c30..8f9488557 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -754,6 +754,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); ) ) else: + H.append('
') H.append( _ue_table_ues( parcours, @@ -783,7 +784,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); """ ) - + H.append("
") H.append("
") # formation_ue_list if ues_externes: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index ecd8b0ab0..b74eba0e3 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1703,8 +1703,11 @@ ul.notes_ue_list { padding-bottom: 1em; font-weight: bold; } +.formation_classic_infos ul.notes_ue_list { + padding-top: 0px; +} -li.notes_ue_list { +.formation_classic_infos li.notes_ue_list { margin-top: 9px; list-style-type: none; border: 1px solid maroon; @@ -1761,6 +1764,11 @@ ul.notes_module_list { font-style: normal; } +div.ue_list_tit_sem { + font-size: 120%; + font-weight: bold; +} + .notes_ue_list a.stdlink { color: #001084; text-decoration: underline; From 29b5d54d222df3cc334a4bca4c2a3023916e8d3d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Feb 2022 20:12:20 +0100 Subject: [PATCH 44/74] =?UTF-8?q?Prise=20en=20compte=20UE=20capitalis?= =?UTF-8?q?=C3=A9es=20lorsque=20non=20inscrit=20dans=20le=20sem.=20courant?= =?UTF-8?q?.=20Affichage=20sur=20bulletins=20classiques.=20Capitalisation?= =?UTF-8?q?=20en=20BUT=20avec=20ECTS.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_common.py | 61 +++++++++++++++++----- app/models/moduleimpls.py | 31 +++++++++-- app/scodoc/sco_bulletins.py | 8 +-- app/scodoc/sco_codes_parcours.py | 2 +- app/scodoc/sco_formsemestre_status.py | 2 +- app/scodoc/sco_formsemestre_validation.py | 10 ++-- app/scodoc/sco_parcours_dut.py | 4 +- app/static/css/scodoc.css | 6 +-- app/static/icons/scologo_img.png | Bin 12724 -> 37970 bytes app/views/notes.py | 25 +++++---- 10 files changed, 108 insertions(+), 41 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index f13201262..7fa75c1b7 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,18 +9,22 @@ from functools import cached_property import numpy as np import pandas as pd +from flask import g, url_for + from app import log from app.comp.aux_stats import StatsMoyenne from app.comp import moy_sem from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, Identite, ModuleImpl -from app.models import FormSemestreUECoef +from app.models import FormSemestre, FormSemestreUECoef +from app.models import Identite +from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns from app.scodoc import sco_utils as scu from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, DEF +from app.scodoc.sco_exceptions import ScoValueError # Il faut bien distinguer # - ce qui est caché de façon persistente (via redis): @@ -206,12 +210,18 @@ class ResultatsSemestre(ResultatsCache): 0.0, min(self.etud_moy_gen[etudid], 20.0) ) - def _get_etud_ue_cap(self, etudid, ue): - """""" + def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict: + """Donne les informations sur la capitalisation de l'UE ue pour cet étudiant. + Résultat: + Si pas capitalisée: None + Si capitalisée: un dict, avec les colonnes de validation. + """ capitalisations = self.validations.ue_capitalisees.loc[etudid] if isinstance(capitalisations, pd.DataFrame): ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code] - if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty: + if ue_cap.empty: + return None + if isinstance(ue_cap, pd.DataFrame): # si plusieurs fois capitalisée, prend le max cap_idx = ue_cap["moy_ue"].values.argmax() ue_cap = ue_cap.iloc[cap_idx] @@ -219,8 +229,9 @@ class ResultatsSemestre(ResultatsCache): if capitalisations["ue_code"] == ue.ue_code: ue_cap = capitalisations else: - ue_cap = None - return ue_cap + return None + # converti la Series en dict, afin que les np.int64 reviennent en int + return ue_cap.to_dict() def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict: """L'état de l'UE pour cet étudiant. @@ -253,17 +264,41 @@ class ResultatsSemestre(ResultatsCache): ) if etudid in self.validations.ue_capitalisees.index: ue_cap = self._get_etud_ue_cap(etudid, ue) - if ( - ue_cap is not None - and not ue_cap.empty - and not np.isnan(ue_cap["moy_ue"]) - ): + if ue_cap and not np.isnan(ue_cap["moy_ue"]): was_capitalized = True if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): moy_ue = ue_cap["moy_ue"] is_capitalized = True - coef_ue = self.etud_coef_ue_df[ue_id][etudid] + # Coef l'UE dans le semestre courant: + if self.is_apc: + # utilise les ECTS comme coef. + coef_ue = ue.ects + else: + # formations classiques + coef_ue = self.etud_coef_ue_df[ue_id][etudid] + if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante + if self.is_apc: + # Coefs de l'UE capitalisée en formation APC: donné par ses ECTS + ue_capitalized = UniteEns.query.get(ue_cap["ue_id"]) + coef_ue = ue_capitalized.ects + if coef_ue is None: + orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"]) + raise ScoValueError( + f"""L'UE capitalisée {ue_capitalized.acronyme} + du semestre {orig_sem.titre_annee()} + n'a pas d'indication d'ECTS. + Corrigez ou faite corriger le programme + via cette page. + """ + ) + else: + # Coefs de l'UE capitalisée en formation classique: + # va chercher le coef dans le semestre d'origine + coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue( + ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"] + ) return { "is_capitalized": is_capitalized, diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 700dec26e..0aa74ef4b 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -2,6 +2,7 @@ """ScoDoc models: moduleimpls """ import pandas as pd +import flask_sqlalchemy from app import db from app.comp import df_cache @@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model): ) @classmethod - def nb_inscriptions_dans_ue( + def etud_modimpls_in_ue( cls, formsemestre_id: int, etudid: int, ue_id: int - ) -> int: - """Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit""" + ) -> flask_sqlalchemy.BaseQuery: + """moduleimpls de l'UE auxquels l'étudiant est inscrit""" return ModuleImplInscription.query.filter( ModuleImplInscription.etudid == etudid, ModuleImplInscription.moduleimpl_id == ModuleImpl.id, ModuleImpl.formsemestre_id == formsemestre_id, ModuleImpl.module_id == Module.id, Module.ue_id == ue_id, - ).count() + ) + + @classmethod + def nb_inscriptions_dans_ue( + cls, formsemestre_id: int, etudid: int, ue_id: int + ) -> int: + """Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit""" + return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count() + + @classmethod + def sum_coefs_modimpl_ue( + cls, formsemestre_id: int, etudid: int, ue_id: int + ) -> float: + """Somme des coefficients des modules auxquels l'étudiant est inscrit + dans l'UE du semestre indiqué. + N'utilise que les coefficients, donc inadapté aux formations APC. + """ + return sum( + [ + inscr.modimpl.module.coefficient + for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id) + ] + ) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index ca6748de7..a5d84cf4f 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -291,15 +291,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["matieres_modules"] = {} I["matieres_modules_capitalized"] = {} for ue in ues: + u = ue.copy() + ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) if ( ModuleImplInscription.nb_inscriptions_dans_ue( formsemestre_id, etudid, ue["ue_id"] ) == 0 - ): + ) and not ue_status["is_capitalized"]: + # saute les UE où l'on est pas inscrit et n'avons pas de capitalisation continue - u = ue.copy() - ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) + u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...} if ue["type"] != sco_codes_parcours.UE_SPORT: u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"]) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index f6ca2ff29..bcd6522b1 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -282,7 +282,7 @@ class TypeParcours(object): return [ ue_status for ue_status in ues_status - if ue_status["coef_ue"] > 0 + if ue_status["coef_ue"] and isinstance(ue_status["moy"], float) and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"]) ] diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 2c8465150..4bfa7b725 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1078,7 +1078,7 @@ def formsemestre_status(formsemestre_id=None): "

", ] - if use_ue_coefs: + if use_ue_coefs and not formsemestre.formation.is_apc(): H.append( """

utilise les coefficients d'UE pour calculer la moyenne générale.

diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 987dc962f..c83e1cc4d 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table( else: H.append('en cours') H.append('%s' % ass) # abs - # acronymes UEs auxquelles l'étudiant est inscrit: - # XXX il est probable que l'on doive ici ajouter les - # XXX UE capitalisées + # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé) ues = nt.get_ues_stat_dict(filter_sport=True) cnx = ndb.GetDBConnexion() + etud_ue_status = { + ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues + } ues = [ ue for ue in ues if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"]) + or etud_ue_status[ue["ue_id"]]["is_capitalized"] ] for ue in ues: @@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table( code = decisions_ue[ue["ue_id"]]["code"] else: code = "" - ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) + ue_status = etud_ue_status[ue["ue_id"]] moy_ue = ue_status["moy"] if ue_status else "" explanation_ue = [] # list of strings if code == ADM: diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index d6e244e79..fbf190c52 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object): # pour le DUT, le dernier est toujours S4. # Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1 # (licences et autres formations en 1 seule session)) - self.semestre_non_terminal = ( - self.sem["semestre_id"] != self.parcours.NB_SEM - ) # True | False + self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM if self.sem["semestre_id"] == NO_SEMESTRE_ID: self.semestre_non_terminal = False # Liste des semestres du parcours de cet étudiant: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index b74eba0e3..bd99c80c6 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -287,15 +287,15 @@ div.logo-insidebar { width: 75px; /* la marge fait 130px */ } div.logo-logo { + margin-left: -5px; text-align: center ; } div.logo-logo img { box-sizing: content-box; - margin-top: -10px; - width: 128px; + margin-top: 10px; /* -10px */ + width: 135px; /* 128px */ padding-right: 5px; - margin-left: -75px; } div.sidebar-bottom { margin-top: 10px; diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png index fb0467ae6dc06f2db5371963a391dcb5da83171b..5c710a76ae816308cd3b5ab46a840cea74ee3d6a 100644 GIT binary patch delta 37804 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 UE1#;N&Hw-a07*qoM6N<$g11NsxBvhE delta 12391 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 Z{{d1ufCu*9Zr}g_002ovPDHLkV1jOzoFxDN diff --git a/app/views/notes.py b/app/views/notes.py index cd211ca05..a8c387e4b 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -35,7 +35,7 @@ from operator import itemgetter from xml.etree import ElementTree import flask -from flask import flash, jsonify, render_template, url_for +from flask import abort, flash, jsonify, render_template, url_for from flask import current_app, g, request from flask_login import current_user from werkzeug.utils import redirect @@ -68,10 +68,14 @@ from app.scodoc import sco_utils as scu from app.scodoc import notesdb as ndb from app import log, send_scodoc_alarm -from app.scodoc import scolog from app.scodoc.scolog import logdb -from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType +from app.scodoc.sco_exceptions import ( + AccessDenied, + ScoException, + ScoValueError, + ScoInvalidIdType, +) from app.scodoc import html_sco_header from app.pe import pe_view from app.scodoc import sco_abs @@ -2672,12 +2676,15 @@ def check_integrity_all(): def moduleimpl_list( moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json" ): - data = sco_moduleimpl.moduleimpl_list( - moduleimpl_id=moduleimpl_id, - formsemestre_id=formsemestre_id, - module_id=module_id, - ) - return scu.sendResult(data, format=format) + try: + data = sco_moduleimpl.moduleimpl_list( + moduleimpl_id=moduleimpl_id, + formsemestre_id=formsemestre_id, + module_id=module_id, + ) + return scu.sendResult(data, format=format) + except ScoException: + abort(404) @bp.route("/do_moduleimpl_withmodule_list") # ancien nom From 00fa91e598b12833dbd2669c16c7c3783be44b69 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 27 Feb 2022 20:32:38 +0100 Subject: [PATCH 45/74] Calcul moyenne gen. BUT avec ECTS --- app/comp/moy_sem.py | 31 ++++++++++++++++++++++++++++++- app/comp/res_but.py | 13 +++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 5caa3d393..6a44dbc00 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -30,8 +30,11 @@ import numpy as np import pandas as pd +from flask import g, url_for +from app.scodoc.sco_exceptions import ScoValueError -def compute_sem_moys_apc( + +def compute_sem_moys_apc_using_coefs( etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame ) -> pd.Series: """Calcule les moyennes générales indicatives de tous les étudiants @@ -48,6 +51,32 @@ def compute_sem_moys_apc( return moy_gen +def compute_sem_moys_apc_using_ects( + etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None +) -> pd.Series: + """Calcule les moyennes générales indicatives de tous les étudiants + = moyenne des moyennes d'UE, pondérée par leurs ECTS. + + etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid + ects: liste de floats ou None, 1 par UE + + Result: panda Series, index etudid, valeur float (moyenne générale) + """ + try: + moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects) + except TypeError: + if None in ects: + raise ScoValueError( + f"""Calcul impossible: ECTS des UE manquants ! + voir la page du programme. + """ + ) + else: + raise + return moy_gen + + def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series): """Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique) en tenant compte des ex-aequos. diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 1d01f4f42..f4cdda8ab 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -14,7 +14,7 @@ from app import log from app.comp import moy_ue, moy_sem, inscr_mod from app.comp.res_common import NotesTableCompat from app.comp.bonus_spo import BonusSport -from app.models import ScoDocSiteConfig +from app.models import ScoDocSiteConfig, formsemestre from app.models.ues import UniteEns from app.scodoc.sco_codes_parcours import UE_SPORT @@ -73,7 +73,7 @@ class ResultatsSemestreBUT(NotesTableCompat): ) # Les coefficients d'UE ne sont pas utilisés en APC self.etud_coef_ue_df = pd.DataFrame( - 1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns + 0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns ) # --- Modules de MALUS sur les UEs @@ -103,8 +103,13 @@ class ResultatsSemestreBUT(NotesTableCompat): # Moyenne générale indicative: # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # donc la moyenne indicative) - self.etud_moy_gen = moy_sem.compute_sem_moys_apc( - self.etud_moy_ue, self.modimpl_coefs_df + # self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs( + # self.etud_moy_ue, self.modimpl_coefs_df + # ) + self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects( + self.etud_moy_ue, + [ue.ects for ue in self.ues], + formation_id=self.formsemestre.formation_id, ) # --- UE capitalisées self.apply_capitalisation() From 68680e89d32de6f414220beb5ddd4b4c22c5cc0c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 09:22:17 +0100 Subject: [PATCH 46/74] Exception si erreur connexion vers assistance --- app/scodoc/sco_dump_db.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index b11f63ca8..f9521b292 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -186,18 +186,28 @@ def _send_db(ano_db_name): log("uploading anonymized dump...") files = {"file": (ano_db_name + ".dump", dump)} - r = requests.post( - scu.SCO_DUMP_UP_URL, - files=files, - data={ - "dept_name": sco_preferences.get_preference("DeptName"), - "serial": _get_scodoc_serial(), - "sco_user": str(current_user), - "sent_by": sco_users.user_info(str(current_user))["nomcomplet"], - "sco_version": sco_version.SCOVERSION, - "sco_fullversion": scu.get_scodoc_version(), - }, - ) + try: + r = requests.post( + scu.SCO_DUMP_UP_URL, + files=files, + data={ + "dept_name": sco_preferences.get_preference("DeptName"), + "serial": _get_scodoc_serial(), + "sco_user": str(current_user), + "sent_by": sco_users.user_info(str(current_user))["nomcomplet"], + "sco_version": sco_version.SCOVERSION, + "sco_fullversion": scu.get_scodoc_version(), + }, + ) + except requests.exceptions.ConnectionError as exc: + raise ScoValueError( + """ + Impossible de joindre le serveur d'assistance (scodoc.org). + Veuillez contacter le service informatique de votre établissement pour + corriger la configuration de ScoDoc. Dans la plupart des cas, il + s'agit d'un proxy mal configuré. + """ + ) from exc return r From ef408e5d8eb97f924c4bef98c66b33a2fdb34878 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 11:00:24 +0100 Subject: [PATCH 47/74] Gestion calcul moy gen et capit. BUT si ECTS manquants --- app/comp/moy_sem.py | 11 +++-------- app/comp/res_common.py | 5 ++--- app/models/ues.py | 6 ++++-- app/scodoc/sco_edit_ue.py | 9 +++++---- app/static/css/scodoc.css | 5 ++++- app/templates/pn/form_ues.html | 3 ++- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 6a44dbc00..2aec3b736 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -30,8 +30,7 @@ import numpy as np import pandas as pd -from flask import g, url_for -from app.scodoc.sco_exceptions import ScoValueError +from flask import flash def compute_sem_moys_apc_using_coefs( @@ -66,12 +65,8 @@ def compute_sem_moys_apc_using_ects( moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects) except TypeError: if None in ects: - raise ScoValueError( - f"""Calcul impossible: ECTS des UE manquants ! - voir la page du programme. - """ - ) + flash(f"""Calcul moyenne générale impossible: ECTS des UE manquants !""") + moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index) else: raise return moy_gen diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7fa75c1b7..6a821ac69 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -259,9 +259,8 @@ class ResultatsSemestre(ResultatsCache): cur_moy_ue = self.etud_moy_ue[ue_id][etudid] moy_ue = cur_moy_ue is_capitalized = False # si l'UE prise en compte est une UE capitalisée - was_capitalized = ( - False # s'il y a precedemment une UE capitalisée (pas forcement meilleure) - ) + # s'il y a precedemment une UE capitalisée (pas forcement meilleure): + was_capitalized = False if etudid in self.validations.ue_capitalisees.index: ue_cap = self._get_etud_ue_cap(etudid, ue) if ue_cap and not np.isnan(ue_cap["moy_ue"]): diff --git a/app/models/ues.py b/app/models/ues.py index 09469fb05..2bed88a38 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -54,13 +54,15 @@ class UniteEns(db.Model): 'EXTERNE' if self.is_external else ''})>""" def to_dict(self): - """as a dict, with the same conversions as in ScoDoc7""" + """as a dict, with the same conversions as in ScoDoc7 + (except ECTS: keep None) + """ e = dict(self.__dict__) e.pop("_sa_instance_state", None) # ScoDoc7 output_formators e["ue_id"] = self.id e["numero"] = e["numero"] if e["numero"] else 0 - e["ects"] = e["ects"] if e["ects"] else 0.0 + e["ects"] = e["ects"] e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["code_apogee"] = e["code_apogee"] or "" # pas de None return e diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 8f9488557..73e77dbbd 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable( input_formators={ "type": ndb.int_null_is_zero, "is_external": ndb.bool_or_str, + "ects": ndb.float_null_is_null, }, output_formators={ "numero": ndb.int_null_is_zero, @@ -347,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "type": "float", "title": "ECTS", "explanation": "nombre de crédits ECTS", + "allow_null": not is_apc, # ects requis en APC }, ), ( @@ -933,10 +935,10 @@ def _ue_table_ues( cur_ue_semestre_id = None iue = 0 for ue in ues: - if ue["ects"]: - ue["ects_str"] = ", %g ECTS" % ue["ects"] - else: + if ue["ects"] is None: ue["ects_str"] = "" + else: + ue["ects_str"] = ", %g ECTS" % ue["ects"] if editable: klass = "span_apo_edit" else: @@ -1295,7 +1297,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! (chaque UE doit avoir un acronyme unique dans la formation)""" ) - # On ne peut pas supprimer le code UE: if "ue_code" in args and not args["ue_code"]: del args["ue_code"] diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index bd99c80c6..3b80f7d6d 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list { padding-top: 5px; padding-bottom: 5px; } - +span.missing_ue_ects { + color: red; + font-weight: bold; +} li.module_malus span.formation_module_tit { color: red; font-weight: bold; diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index 0cd3e333b..98c590f7b 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -38,7 +38,8 @@ {% set virg = joiner(", ") %} ( {%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%} - {{ virg() }}{{ue.ects or 0}} ECTS) + {{ virg() }}{{ue.ects if ue.ects is not none + else 'aucun'|safe}} ECTS) From e1db9c542bffc3b6c8620782457e9ee52200795b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 11:47:39 +0100 Subject: [PATCH 48/74] Messages flash flask sur ancioennes pages ScoDoc + warning ECTS BUT --- app/comp/moy_sem.py | 2 +- app/comp/res_common.py | 22 ++++++++++++++++------ app/scodoc/html_sco_header.py | 5 ++++- app/scodoc/sco_edit_ue.py | 2 ++ app/static/css/scodoc.css | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 2aec3b736..db42616c8 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -65,7 +65,7 @@ def compute_sem_moys_apc_using_ects( moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects) except TypeError: if None in ects: - flash(f"""Calcul moyenne générale impossible: ECTS des UE manquants !""") + flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""") moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index) else: raise diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 6a821ac69..7d2eb5ae3 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,7 +9,7 @@ from functools import cached_property import numpy as np import pandas as pd -from flask import g, url_for +from flask import g, flash, url_for from app import log from app.comp.aux_stats import StatsMoyenne @@ -419,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre): """Stats (moy/min/max) sur la moyenne générale""" return StatsMoyenne(self.etud_moy_gen) - def get_ues_stat_dict(self, filter_sport=False): # was get_ues() + def get_ues_stat_dict( + self, filter_sport=False, check_apc_ects=True + ) -> list[dict]: # was get_ues() """Liste des UEs, ordonnée par numero. Si filter_sport, retire les UE de type SPORT. Résultat: liste de dicts { champs UE U stats moyenne UE } """ - ues = [] - for ue in self.formsemestre.query_ues(with_sport=not filter_sport): + ues = self.formsemestre.query_ues(with_sport=not filter_sport) + ues_dict = [] + for ue in ues: d = ue.to_dict() if ue.type != UE_SPORT: moys = self.etud_moy_ue[ue.id] else: moys = None d.update(StatsMoyenne(moys).to_dict()) - ues.append(d) - return ues + ues_dict.append(d) + if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"): + g.checked_apc_ects = True + if None in [ue.ects for ue in ues if ue.type != UE_SPORT]: + flash( + """Calcul moyenne générale impossible: ECTS des UE manquants !""", + category="danger", + ) + return ues_dict def get_modimpls_dict(self, ue_id=None) -> list[dict]: """Liste des modules pour une UE (ou toutes si ue_id==None), diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py index 23a8b8340..653cdb80d 100644 --- a/app/scodoc/html_sco_header.py +++ b/app/scodoc/html_sco_header.py @@ -30,7 +30,7 @@ import html -from flask import g +from flask import render_template from flask import request from flask_login import current_user @@ -280,6 +280,9 @@ def sco_header( if not no_side_bar: H.append(html_sidebar.sidebar()) H.append("""

""") + # En attendant le replacement complet de cette fonction, + # inclusion ici des messages flask + H.append(render_template("flashed_messages.html")) # # Barre menu semestre: H.append(formsemestre_page_title()) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 73e77dbbd..8cc3269a1 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -472,8 +472,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "semestre_id": tf[2]["semestre_idx"], }, ) + flash("UE créée") else: do_ue_edit(tf[2]) + flash("UE modifiée") return flask.redirect( url_for( "notes.ue_table", diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 3b80f7d6d..b3a80640a 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -138,7 +138,7 @@ div.head_message { border-radius: 8px; font-family : arial, verdana, sans-serif ; font-weight: bold; - width: 40%; + width: 70%; text-align: center; } From 546e10c83a333cd215a103d72e5e12d1d1cc5ff2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 15:07:48 +0100 Subject: [PATCH 49/74] Finalise calcul moy. gen. indicative BUT --- app/comp/res_but.py | 2 +- app/comp/res_common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index f4cdda8ab..20e63cba0 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -108,7 +108,7 @@ class ResultatsSemestreBUT(NotesTableCompat): # ) self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects( self.etud_moy_ue, - [ue.ects for ue in self.ues], + [ue.ects for ue in self.ues if ue.type != UE_SPORT], formation_id=self.formsemestre.formation_id, ) # --- UE capitalisées diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 7d2eb5ae3..8fa106f50 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -195,7 +195,7 @@ class ResultatsSemestre(ResultatsCache): if ue_cap["is_capitalized"]: recompute_mg = True coef = ue_cap["coef_ue"] - if not np.isnan(ue_cap["moy"]): + if not np.isnan(ue_cap["moy"]) and coef: sum_notes_ue += ue_cap["moy"] * coef sum_coefs_ue += coef From 5aa896f7932e27d551cdfcae1d36ec275855ba34 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 15:08:32 +0100 Subject: [PATCH 50/74] Bonus Aisne St Quentin + fix bonus Ville d'Avray --- app/comp/bonus_spo.py | 61 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 18c57beb0..99738336e 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -228,6 +228,10 @@ class BonusSportAdditif(BonusSport): else: # necessaire pour éviter bonus négatifs ! bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr) + self.bonus_additif(bonus_moy_arr) + + def bonus_additif(self, bonus_moy_arr: np.array): + "Set bonus_ues et bonus_moy_gen" # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus) if self.formsemestre.formation.is_apc(): # Bonus sur les UE et None sur moyenne générale @@ -306,6 +310,47 @@ class BonusDirect(BonusSportAdditif): proportion_point = 1.0 +class BonusAisneStQuentin(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin + +

Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université de St Quentin non rattachés à une unité d'enseignement. +

+
    +
  • Si la note est >= 10 et < 12.1, bonus de 0.1 point
  • +
  • Si la note est >= 12.1 et < 14.1, bonus de 0.2 point
  • +
  • Si la note est >= 14.1 et < 16.1, bonus de 0.3 point
  • +
  • Si la note est >= 16.1 et < 18.1, bonus de 0.4 point
  • +
  • Si la note est >= 18.1, bonus de 0.5 point
  • +
+

+ Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par + l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE). +

+ """ + + name = "bonus_iutstq" + displayed_name = "IUT de Saint-Quentin" + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + if 0 in sem_modimpl_moys_inscrits.shape: + # pas d'étudiants ou pas d'UE ou pas de module... + return + # Calcule moyenne pondérée des notes de sport: + bonus_moy_arr = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 + bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5 + bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4 + bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3 + bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2 + bonus_moy_arr[bonus_moy_arr >= 10] = 0.1 + + self.bonus_additif(bonus_moy_arr) + + class BonusAmiens(BonusSportAdditif): """Bonus IUT Amiens pour les modules optionnels (sport, culture, ...). @@ -774,21 +819,19 @@ class BonusVilleAvray(BonusSport): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): """calcul du bonus""" + if 0 in sem_modimpl_moys_inscrits.shape: + # pas d'étudiants ou pas d'UE ou pas de module... + return # Calcule moyenne pondérée des notes de sport: bonus_moy_arr = np.sum( sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) - bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1 - bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 + bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3 + bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 + bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1 - # Bonus moyenne générale, et 0 sur les UE - self.bonus_moy_gen = pd.Series(bonus_moy_arr, index=self.etuds_idx, dtype=float) - if self.bonus_max is not None: - # Seuil: bonus (sur moy. gen.) limité à bonus_max points - self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_max) - - # Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs. + self.bonus_additif(bonus_moy_arr) class BonusIUTV(BonusSportAdditif): From f7c90397a890ed7c3f0806b9115b64c2aeb9ca3e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 15:12:32 +0100 Subject: [PATCH 51/74] Enhance scodoc7 decorator: FileStorage arguments --- app/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/decorators.py b/app/decorators.py index 8ebf5deab..220ece566 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -193,7 +193,7 @@ def scodoc7func(func): # necessary for db ids and boolean values try: v = int(v) - except ValueError: + except (ValueError, TypeError): pass pos_arg_values.append(v) # current_app.logger.info("pos_arg_values=%s" % pos_arg_values) From bee7b74f170b4c5dd1d786c1eb5c56c51d753cdc Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 15:18:21 +0100 Subject: [PATCH 52/74] =?UTF-8?q?Fichier=20oubli=C3=A9=20(flask=20flash)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/templates/flashed_messages.html | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/templates/flashed_messages.html diff --git a/app/templates/flashed_messages.html b/app/templates/flashed_messages.html new file mode 100644 index 000000000..5ded75245 --- /dev/null +++ b/app/templates/flashed_messages.html @@ -0,0 +1,9 @@ +{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #} +{# -*- mode: jinja-html -*- #} +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} + + {% endfor %} + {% endwith %} +
\ No newline at end of file From 732a4c5ce51397a3035d8783ce3cbf535c6acf23 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:25:18 +0100 Subject: [PATCH 53/74] code cleaning --- app/scodoc/sco_cache.py | 76 ++++++++--------------------------------- 1 file changed, 15 insertions(+), 61 deletions(-) diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 5fd0cec68..6330849c5 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -33,17 +33,12 @@ """ -# API ScoDoc8 pour les caches: -# sco_cache.NotesTableCache.get( formsemestre_id) -# => sco_cache.NotesTableCache.get(formsemestre_id) +# API pour les caches: +# sco_cache.MyCache.get( formsemestre_id) +# => sco_cache.MyCache.get(formsemestre_id) # -# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None) -# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False) -# -# -# Nouvelles fonctions: -# sco_cache.NotesTableCache.delete(formsemestre_id) -# sco_cache.NotesTableCache.delete_many(formsemestre_id_list) +# sco_cache.MyCache.delete(formsemestre_id) +# sco_cache.MyCache.delete_many(formsemestre_id_list) # # Bulletins PDF: # sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version) @@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache): duration = 12 * 60 * 60 # ttl 12h -class NotesTableCache(ScoDocCache): - """Cache pour les NotesTable - Clé: formsemestre_id - Valeur: NotesTable instance - """ - - prefix = "NT" - - @classmethod - def get(cls, formsemestre_id, compute=True): - """Returns NotesTable for this formsemestre - Search in local cache (g.nt_cache) or global app cache (eg REDIS) - If not in cache: - If compute is True, build it and cache it - Else return None - """ - # try local cache (same request) - if not hasattr(g, "nt_cache"): - g.nt_cache = {} - else: - if formsemestre_id in g.nt_cache: - return g.nt_cache[formsemestre_id] - # try REDIS - key = cls._get_key(formsemestre_id) - nt = CACHE.get(key) - if nt: - g.nt_cache[formsemestre_id] = nt # cache locally (same request) - return nt - if not compute: - return None - # Recompute requested table: - from app.scodoc import notes_table - - t0 = time.time() - nt = notes_table.NotesTable(formsemestre_id) - t1 = time.time() - _ = cls.set(formsemestre_id, nt) # cache in REDIS - t2 = time.time() - log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)") - g.nt_cache[formsemestre_id] = nt - return nt - - def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False) formsemestre_id=None, pdfonly=False ): @@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa if not pdfonly: # Delete cached notes and evaluations - NotesTableCache.delete_many(formsemestre_ids) if formsemestre_id: for fid in formsemestre_ids: EvaluationCache.invalidate_sem(fid) - if hasattr(g, "nt_cache") and fid in g.nt_cache: - del g.nt_cache[fid] + if ( + hasattr(g, "formsemestre_results_cache") + and fid in g.formsemestre_results_cache + ): + del g.formsemestre_results_cache[fid] + else: # optimization when we invalidate all evaluations: EvaluationCache.invalidate_all_sems() - if hasattr(g, "nt_cache"): - del g.nt_cache + if hasattr(g, "formsemestre_results_cache"): + del g.formsemestre_results_cache SemInscriptionsCache.delete_many(formsemestre_ids) - + ResultatsSemestreCache.delete_many(formsemestre_ids) + ValidationsSemestreCache.delete_many(formsemestre_ids) SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) - ResultatsSemestreCache.delete_many(formsemestre_ids) - ValidationsSemestreCache.delete_many(formsemestre_ids) class DefferedSemCacheManager: From 8330009dcf1dedf6fb7ad3288375ae3e73171d84 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:26:13 +0100 Subject: [PATCH 54/74] =?UTF-8?q?En=20BUT,=20remet=20S1=20si=20semestre=20?= =?UTF-8?q?non=20sp=C3=A9cifi=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_sem.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/comp/res_sem.py b/app/comp/res_sem.py index 607ad1681..e27a157c1 100644 --- a/app/comp/res_sem.py +++ b/app/comp/res_sem.py @@ -8,11 +8,13 @@ """ from flask import g +from app import db from app.comp.jury import ValidationsSemestre from app.comp.res_common import ResultatsSemestre from app.comp.res_classic import ResultatsSemestreClassic from app.comp.res_but import ResultatsSemestreBUT from app.models.formsemestre import FormSemestre +from app.scodoc import sco_cache def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: @@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: Search in local cache (g.formsemestre_result_cache) If not in cache, build it and cache it. """ + is_apc = formsemestre.formation.is_apc() + if is_apc and formsemestre.semestre_id == -1: + formsemestre.semestre_id = 1 + db.session.add(formsemestre) + db.session.commit() + sco_cache.invalidate_formsemestre(formsemestre.id) + # --- Try local cache (within the same request context) if not hasattr(g, "formsemestre_results_cache"): g.formsemestre_results_cache = {} @@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre: if formsemestre.id in g.formsemestre_results_cache: return g.formsemestre_results_cache[formsemestre.id] - klass = ( - ResultatsSemestreBUT - if formsemestre.formation.is_apc() - else ResultatsSemestreClassic - ) + klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre) return g.formsemestre_results_cache[formsemestre.id] From 0e7f2f4deb160ead6d595fb6187871fdef8132ea Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:27:27 +0100 Subject: [PATCH 55/74] flash --- app/scodoc/sco_dump_db.py | 2 ++ app/scodoc/sco_formsemestre_status.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index f9521b292..4b55b41f6 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -51,6 +51,7 @@ import fcntl import subprocess import requests +from flask import flash from flask_login import current_user import app.scodoc.notesdb as ndb @@ -124,6 +125,7 @@ def sco_dump_and_send_db(): fcntl.flock(x, fcntl.LOCK_UN) log("sco_dump_and_send_db: done.") + flash("Données envoyées au serveur d'assistance") return "\n".join(H) + html_sco_header.sco_footer() diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 4bfa7b725..2080e9572 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -987,7 +987,6 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind def formsemestre_status(formsemestre_id=None): """Tableau de bord semestre HTML""" # porté du DTML - cnx = ndb.GetDBConnexion() sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) modimpls = sco_moduleimpl.moduleimpl_withmodule_list( formsemestre_id=formsemestre_id From 8b5a99657106e9f9a67b2ab1f61262641fcce374 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 16:28:08 +0100 Subject: [PATCH 56/74] Semestre BUT: ne propose pas indice -1 --- app/scodoc/sco_formsemestre_edit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 81dbd5659..3e1c92fbb 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False): # en APC, ne permet pas de changer de semestre semestre_id_list = [formsemestre.semestre_id] else: - semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) + semestre_id_list = list(range(1, NB_SEM + 1)) + if not formation.is_apc(): + # propose "pas de semestre" seulement en classique + semestre_id_list.insert(0, -1) semestre_id_labels = [] for sid in semestre_id_list: From e993599b39d15567d07db8cc8de87d9b0011cc89 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 17:57:12 +0100 Subject: [PATCH 57/74] =?UTF-8?q?Restreint=20edition=20modules=20semestres?= =?UTF-8?q?=20BUT=20aux=20module=20du=20m=C3=AAme=20sem.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_edit.py | 30 ++++++++++++++++++++--------- app/static/js/formsemestre_edit.js | 14 ++++++++++++++ 2 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 app/static/js/formsemestre_edit.js diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 3e1c92fbb..f056a3e62 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -78,7 +78,7 @@ def formsemestre_createwithmodules(): H = [ html_sco_header.sco_header( page_title="Création d'un semestre", - javascripts=["libjs/AutoSuggest.js"], + javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", ), @@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id): H = [ html_sco_header.html_sem_header( "Modification du semestre", - javascripts=["libjs/AutoSuggest.js"], + javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"], cssstyles=["css/autosuggest_inquisitor.css"], bodyOnLoad="init_tf_form('')", ) @@ -344,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False): "explanation": "en BUT, on ne peut pas modifier le semestre après création" if formation.is_apc() else "", + "attributes": ['onchange="change_semestre_id();"'] + if formation.is_apc() + else "", }, ), ) @@ -496,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False): { "input_type": "boolcheckbox", "title": "", - "explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations", + "explanation": """Autoriser tous les enseignants associés + à un module à y créer des évaluations""", }, ), ( @@ -537,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False): ] nbmod = 0 - if edit: - templ_sep = "%(label)sResponsableInscrire" - else: - templ_sep = "%(label)sResponsable" + for semestre_id in semestre_ids: + if formation.is_apc(): + # pour restreindre l'édition aux module du semestre sélectionné + tr_class = 'class="sem{semestre_id}"' + else: + tr_class = "" + if edit: + templ_sep = f"""%(label)sResponsableInscrire""" + else: + templ_sep = ( + f"""%(label)sResponsable""" + ) modform.append( ( "sep", @@ -591,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False): ) fcg += "" itemtemplate = ( - """%(label)s%(elem)s""" + f"""%(label)s%(elem)s""" + fcg + "" ) else: - itemtemplate = """%(label)s%(elem)s""" + itemtemplate = f"""%(label)s%(elem)s""" modform.append( ( "MI" + str(mod["module_id"]), diff --git a/app/static/js/formsemestre_edit.js b/app/static/js/formsemestre_edit.js new file mode 100644 index 000000000..3394d659c --- /dev/null +++ b/app/static/js/formsemestre_edit.js @@ -0,0 +1,14 @@ +// Formulaire formsemestre_createwithmodules + +function change_semestre_id() { + var semestre_id = $("#tf_semestre_id")[0].value; + for (var i = -1; i < 12; i++) { + $(".sem" + i).hide(); + } + $(".sem" + semestre_id).show(); +} + + +$(window).on('load', function () { + change_semestre_id(); +}); \ No newline at end of file From b56a20643d5366322c27839a542aeda604398d1e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 20:01:24 +0100 Subject: [PATCH 58/74] largeur colonne codes modules --- app/static/css/scodoc.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index b3a80640a..d735077a4 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits { text-align: center; } td.formsemestre_status_code { - width: 2em; + /* width: 2em; */ padding-right: 1em; } From 13b40936b8cedce19b39b89455a314c8cb49af3e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 28 Feb 2022 20:02:10 +0100 Subject: [PATCH 59/74] =?UTF-8?q?Nouveau=20calcul=20(correct=3F)=20de=20la?= =?UTF-8?q?=20moyenne=20de=20mati=C3=A8re=20en=20classic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mat.py | 6 ++-- app/comp/moy_ue.py | 67 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/app/comp/moy_mat.py b/app/comp/moy_mat.py index e5ba903c2..0a7522637 100644 --- a/app/comp/moy_mat.py +++ b/app/comp/moy_mat.py @@ -40,13 +40,11 @@ def compute_mat_moys_classic( 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, + etud_moy_mat = moy_ue.compute_mat_moys_classic( 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 + matiere_moy[matiere_id] = etud_moy_mat return matiere_moy diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 563fb3b1d..efbe7cd34 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -294,7 +294,8 @@ def compute_ue_moys_classic( modimpl_coefs: np.array, modimpl_mask: np.array, ) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]: - """Calcul de la moyenne d'UE en mode classique. + """Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...). + La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR NI non inscrit à (au moins un) module de cette UE NA pas de notes disponibles @@ -363,7 +364,7 @@ def compute_ue_moys_classic( modimpl_coefs_etuds_no_nan_stacked = np.stack( [modimpl_coefs_etuds_no_nan.T] * nb_ues ) - # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions + # nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions: coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2) if coefs.dtype == np.object: # arrive sur des tableaux vides coefs = coefs.astype(np.float) @@ -408,6 +409,68 @@ def compute_ue_moys_classic( return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df +def compute_mat_moys_classic( + sem_matrix: np.array, + modimpl_inscr_df: pd.DataFrame, + modimpl_coefs: np.array, + modimpl_mask: np.array, +) -> pd.Series: + """Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE + + La moyenne est un nombre (note/20 ou NaN. + + Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui + permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt). + + sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls) + ndarray (etuds x modimpls) + (floats avec des NaN) + etuds : listes des étudiants (dim. 0 de la matrice) + modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) + modimpl_coefs: vecteur des coefficients de modules + modimpl_mask: masque des modimpls à prendre en compte + + Résultat: + - moyennes: pd.Series, index etudid + """ + if (not len(modimpl_mask)) or ( + sem_matrix.shape[0] == 0 + ): # aucun module ou aucun étudiant + # etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df + return pd.Series( + [0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index + ) + # Restreint aux modules sélectionnés: + sem_matrix = sem_matrix[:, modimpl_mask] + modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask] + modimpl_coefs = modimpl_coefs[modimpl_mask] + + nb_etuds, nb_modules = sem_matrix.shape + assert len(modimpl_coefs) == nb_modules + + # Enlève les NaN du numérateur: + sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0) + # Ne prend pas en compte les notes des étudiants non inscrits au module: + # Annule les notes: + sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0) + # Annule les coefs des modules où l'étudiant n'est pas inscrit: + modimpl_coefs_etuds = np.where( + modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0 + ) + # Annule les coefs des modules NaN (nb_etuds x nb_mods) + modimpl_coefs_etuds_no_nan = np.where( + np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds + ) + if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides + modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float) + + etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum( + axis=1 + ) / modimpl_coefs_etuds_no_nan.sum(axis=1) + + return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index) + + def compute_malus( formsemestre: FormSemestre, sem_modimpl_moys: np.array, From 7edd0511835ca58d8bcb551541632808ac56b38a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 09:34:05 +0100 Subject: [PATCH 60/74] Fix: bonus St Quentin / Ville d'Avray --- app/comp/bonus_spo.py | 17 +++++++++++------ sco_version.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 99738336e..cb3d93e4f 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -338,9 +338,12 @@ class BonusAisneStQuentin(BonusSportAdditif): # pas d'étudiants ou pas d'UE ou pas de module... return # Calcule moyenne pondérée des notes de sport: - bonus_moy_arr = np.sum( - sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + bonus_moy_arr = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False) + bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5 bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4 @@ -823,9 +826,11 @@ class BonusVilleAvray(BonusSport): # pas d'étudiants ou pas d'UE ou pas de module... return # Calcule moyenne pondérée des notes de sport: - bonus_moy_arr = np.sum( - sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + bonus_moy_arr = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False) bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0 bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3 bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2 diff --git a/sco_version.py b/sco_version.py index b47a266d7..fbbf5bc82 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.66" +SCOVERSION = "9.1.67" SCONAME = "ScoDoc" From c5c0b510ec599a2486d4d39d9bc4519c7c51d41d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 09:48:37 +0100 Subject: [PATCH 61/74] filename export formations --- app/scodoc/sco_formations.py | 8 +++++++- app/scodoc/sco_utils.py | 21 ++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index caeeb707b..573c95dfb 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -151,8 +151,14 @@ def formation_export( if mod["ects"] is None: del mod["ects"] + filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}" return scu.sendResult( - F, name="formation", format=format, force_outer_xml_tag=False, attached=True + F, + name="formation", + format=format, + force_outer_xml_tag=False, + attached=True, + filename=filename, ) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index b3d99ac3e..7af19bc19 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -645,21 +645,30 @@ class ScoDocJSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) -def sendJSON(data, attached=False): +def sendJSON(data, attached=False, filename=None): js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) return send_file( - js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached + js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached ) -def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True): +def sendXML( + data, + tagname=None, + force_outer_xml_tag=True, + attached=False, + quote=True, + filename=None, +): if type(data) != list: data = [data] # always list-of-dicts if force_outer_xml_tag: data = [{tagname: data}] tagname += "_list" doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote) - return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached) + return send_file( + doc, filename=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached + ) def sendResult( @@ -669,6 +678,7 @@ def sendResult( force_outer_xml_tag=True, attached=False, quote_xml=True, + filename=None, ): if (format is None) or (format == "html"): return data @@ -679,9 +689,10 @@ def sendResult( force_outer_xml_tag=force_outer_xml_tag, attached=attached, quote=quote_xml, + filename=filename, ) elif format == "json": - return sendJSON(data, attached=attached) + return sendJSON(data, attached=attached, filename=filename) else: raise ValueError("invalid format: %s" % format) From 6943ccb8729823ad9f73e8b76e6f03ae5db2b8ad Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:16:34 +0100 Subject: [PATCH 62/74] typo (sel. modules BUT) --- app/scodoc/sco_formsemestre_edit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index f056a3e62..7174cfc3d 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -545,7 +545,7 @@ def do_formsemestre_createwithmodules(edit=False): for semestre_id in semestre_ids: if formation.is_apc(): # pour restreindre l'édition aux module du semestre sélectionné - tr_class = 'class="sem{semestre_id}"' + tr_class = f'class="sem{semestre_id}"' else: tr_class = "" if edit: From 10c96ad683fa0ca1a899f160588e1e6b769023c8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:21:15 +0100 Subject: [PATCH 63/74] PE: check submitted template (utf8) --- app/pe/pe_view.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 5a98d375f..5af1a5754 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -36,6 +36,7 @@ """ from flask import send_file, request +from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu from app.scodoc import sco_formsemestre @@ -97,8 +98,12 @@ def pe_view_sem_recap( template_latex = "" # template fourni via le formulaire Web if avis_tmpl_file: - template_latex = avis_tmpl_file.read().decode('utf-8') - template_latex = template_latex + try: + template_latex = avis_tmpl_file.read().decode("utf-8") + except UnicodeDecodeError as e: + raise ScoValueError( + "Données (template) invalides (caractères non UTF8 ?)" + ) from e else: # template indiqué dans préférences ScoDoc ? template_latex = pe_avislatex.get_code_latex_from_scodoc_preference( @@ -114,7 +119,7 @@ def pe_view_sem_recap( footer_latex = "" # template fourni via le formulaire Web if footer_tmpl_file: - footer_latex = footer_tmpl_file.read().decode('utf-8') + footer_latex = footer_tmpl_file.read().decode("utf-8") footer_latex = footer_latex else: footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( From f0e731d151fbb2a28e0e65a3a55650f0065ec27a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:33:53 +0100 Subject: [PATCH 64/74] Fix: bulletin classique quand coef UE None --- app/scodoc/sco_bulletins.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index a5d84cf4f..47947bd36 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -323,9 +323,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): if ue_status["coef_ue"] != None: u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"]) else: - # C'est un bug: - log("u=" + pprint.pformat(u)) - raise Exception("invalid None coef for ue") + u["coef_ue_txt"] = "-" if ( dpv From 523ad7ad2a19caed963f9a5170ba6930a35afa67 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 10:40:38 +0100 Subject: [PATCH 65/74] Modif bonus La Rochelle --- app/comp/bonus_spo.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index cb3d93e4f..aa7697d5a 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -198,7 +198,10 @@ class BonusSportAdditif(BonusSport): à la moyenne générale du semestre déjà obtenue par l'étudiant. """ - seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés + seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte + seuil_comptage = ( + None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen) + ) proportion_point = 0.05 # multiplie les points au dessus du seuil def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): @@ -211,10 +214,13 @@ class BonusSportAdditif(BonusSport): if 0 in sem_modimpl_moys_inscrits.shape: # pas d'étudiants ou pas d'UE ou pas de module... return + seuil_comptage = ( + self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage + ) bonus_moy_arr = np.sum( np.where( sem_modimpl_moys_inscrits > self.seuil_moy_gen, - (sem_modimpl_moys_inscrits - self.seuil_moy_gen) + (sem_modimpl_moys_inscrits - self.seuil_comptage) * self.proportion_point, 0.0, ), @@ -607,8 +613,9 @@ class BonusLaRochelle(BonusSportAdditif): name = "bonus_iutlr" displayed_name = "IUT de La Rochelle" - seuil_moy_gen = 10.0 # tous les points sont comptés - proportion_point = 0.01 + seuil_moy_gen = 10.0 # si bonus > 10, + seuil_comptage = 0.0 # tous les points sont comptés + proportion_point = 0.01 # 1% class BonusLeHavre(BonusSportMultiplicatif): From c0719df0c0f0777c79fa3ff27dbda6250c30e194 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 1 Mar 2022 19:27:03 +0100 Subject: [PATCH 66/74] noms modules sur menu saisie absences --- app/scodoc/sco_edit_module.py | 7 +++++-- app/views/absences.py | 5 ++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 9ea4e2fe7..63bcb72ab 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -562,7 +562,7 @@ def module_edit(module_id=None): "code", { "size": 10, - "explanation": "code du module (doit être unique dans la formation)", + "explanation": "code du module (issu du programme, exemple M1203 ou R2.01. Doit être unique dans la formation)", "allow_null": False, "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( val, field, formation_id, module_id=module_id @@ -701,7 +701,10 @@ def module_edit(module_id=None): { "title": "Code Apogée", "size": 25, - "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP + séparés par des virgules (ce code est propre à chaque établissement, se rapprocher + du référent Apogée). + """, "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), diff --git a/app/views/absences.py b/app/views/absences.py index cf8de2c18..bdbae1458 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -611,8 +611,7 @@ def SignaleAbsenceGrSemestre( """\n""" % { "modimpl_id": modimpl["moduleimpl_id"], - "modname": modimpl["module"]["code"] - or "" + "modname": (modimpl["module"]["code"] or "") + " " + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"]), "sel": sel, @@ -624,7 +623,7 @@ def SignaleAbsenceGrSemestre( sel = "selected" # aucun module specifie H.append( """

-Module concerné par ces absences (%(optionel_txt)s): +Module concerné par ces absences (%(optionel_txt)s):