diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 0a95621e8..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. +

+ +

+ 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, ...). @@ -705,6 +750,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): @@ -773,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): diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 5caa3d393..db42616c8 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -30,8 +30,10 @@ import numpy as np import pandas as pd +from flask import flash -def compute_sem_moys_apc( + +def compute_sem_moys_apc_using_coefs( etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame ) -> pd.Series: """Calcule les moyennes générales indicatives de tous les étudiants @@ -48,6 +50,28 @@ def compute_sem_moys_apc( return moy_gen +def compute_sem_moys_apc_using_ects( + etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None +) -> pd.Series: + """Calcule les moyennes générales indicatives de tous les étudiants + = moyenne des moyennes d'UE, pondérée par leurs ECTS. + + etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid + ects: liste de floats ou None, 1 par UE + + Result: panda Series, index etudid, valeur float (moyenne générale) + """ + try: + moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects) + except TypeError: + if None in ects: + flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""") + moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index) + else: + raise + return moy_gen + + def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series): """Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique) en tenant compte des ex-aequos. diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 1d01f4f42..20e63cba0 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 if ue.type != UE_SPORT], + formation_id=self.formsemestre.formation_id, ) # --- UE capitalisées self.apply_capitalisation() diff --git a/app/comp/res_common.py b/app/comp/res_common.py index f13201262..8fa106f50 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -9,18 +9,22 @@ from functools import cached_property import numpy as np import pandas as pd +from flask import g, flash, url_for + from app import log from app.comp.aux_stats import StatsMoyenne from app.comp import moy_sem from app.comp.res_cache import ResultatsCache from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults -from app.models import FormSemestre, Identite, ModuleImpl -from app.models import FormSemestreUECoef +from app.models import FormSemestre, FormSemestreUECoef +from app.models import Identite +from app.models import ModuleImpl, ModuleImplInscription from app.models.ues import UniteEns from app.scodoc import sco_utils as scu from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_codes_parcours import UE_SPORT, DEF +from app.scodoc.sco_exceptions import ScoValueError # Il faut bien distinguer # - ce qui est caché de façon persistente (via redis): @@ -191,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 @@ -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. @@ -248,22 +259,45 @@ class ResultatsSemestre(ResultatsCache): cur_moy_ue = self.etud_moy_ue[ue_id][etudid] moy_ue = cur_moy_ue is_capitalized = False # si l'UE prise en compte est une UE capitalisée - was_capitalized = ( - False # s'il y a precedemment une UE capitalisée (pas forcement meilleure) - ) + # s'il y a precedemment une UE capitalisée (pas forcement meilleure): + was_capitalized = False if etudid in self.validations.ue_capitalisees.index: ue_cap = self._get_etud_ue_cap(etudid, ue) - if ( - ue_cap is not None - and not ue_cap.empty - and not np.isnan(ue_cap["moy_ue"]) - ): + if ue_cap and not np.isnan(ue_cap["moy_ue"]): was_capitalized = True if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): moy_ue = ue_cap["moy_ue"] is_capitalized = True - coef_ue = self.etud_coef_ue_df[ue_id][etudid] + # Coef l'UE dans le semestre courant: + if self.is_apc: + # utilise les ECTS comme coef. + coef_ue = ue.ects + else: + # formations classiques + coef_ue = self.etud_coef_ue_df[ue_id][etudid] + if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante + if self.is_apc: + # Coefs de l'UE capitalisée en formation APC: donné par ses ECTS + ue_capitalized = UniteEns.query.get(ue_cap["ue_id"]) + coef_ue = ue_capitalized.ects + if coef_ue is None: + orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"]) + raise ScoValueError( + f"""L'UE capitalisée {ue_capitalized.acronyme} + du semestre {orig_sem.titre_annee()} + n'a pas d'indication d'ECTS. + Corrigez ou faite corriger le programme + via cette page. + """ + ) + else: + # Coefs de l'UE capitalisée en formation classique: + # va chercher le coef dans le semestre d'origine + coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue( + ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"] + ) return { "is_capitalized": is_capitalized, @@ -385,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/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) 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/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/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/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_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) 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_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 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 = [ ( diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 061bf43c6..8cc3269a1 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -29,7 +29,7 @@ """ import flask -from flask import url_for, render_template +from flask import flash, render_template, url_for from flask import g, request from flask_login import current_user @@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable( input_formators={ "type": ndb.int_null_is_zero, "is_external": ndb.bool_or_str, + "ects": ndb.float_null_is_null, }, output_formators={ "numero": ndb.int_null_is_zero, @@ -107,8 +108,6 @@ def ue_list(*args, **kw): def do_ue_create(args): "create an ue" - from app.scodoc import sco_formations - cnx = ndb.GetDBConnexion() # check duplicates ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]}) @@ -117,6 +116,14 @@ def do_ue_create(args): f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! (chaque UE doit avoir un acronyme unique dans la formation)""" ) + if not "ue_code" in args: + # évite les conflits de code + while True: + cursor = db.session.execute("select notes_newid_ucod();") + code = cursor.fetchone()[0] + if UniteEns.query.filter_by(ue_code=code).count() == 0: + break + args["ue_code"] = code # create ue_id = _ueEditor.create(cnx, args) @@ -128,6 +135,8 @@ def do_ue_create(args): formation = Formation.query.get(args["formation_id"]) formation.invalidate_module_coefs() # news + ue = UniteEns.query.get(ue_id) + flash(f"UE créée (code {ue.ue_code})") formation = Formation.query.get(args["formation_id"]) sco_news.add( typ=sco_news.NEWS_FORM, @@ -339,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "type": "float", "title": "ECTS", "explanation": "nombre de crédits ECTS", + "allow_null": not is_apc, # ects requis en APC }, ), ( @@ -462,8 +472,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "semestre_id": tf[2]["semestre_idx"], }, ) + flash("UE créée") else: do_ue_edit(tf[2]) + flash("UE modifiée") return flask.redirect( url_for( "notes.ue_table", @@ -746,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); ) ) else: + H.append('
') H.append( _ue_table_ues( parcours, @@ -775,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); """ ) - + H.append("
") H.append("
") # formation_ue_list if ues_externes: @@ -924,10 +937,10 @@ def _ue_table_ues( cur_ue_semestre_id = None iue = 0 for ue in ues: - if ue["ects"]: - ue["ects_str"] = ", %g ECTS" % ue["ects"] - else: + if ue["ects"] is None: ue["ects_str"] = "" + else: + ue["ects_str"] = ", %g ECTS" % ue["ects"] if editable: klass = "span_apo_edit" else: @@ -1286,7 +1299,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! (chaque UE doit avoir un acronyme unique dans la formation)""" ) - # On ne peut pas supprimer le code UE: if "ue_code" in args and not args["ue_code"]: del args["ue_code"] diff --git a/app/scodoc/sco_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/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_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: 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/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/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/static/css/scodoc.css b/app/static/css/scodoc.css index ecd8b0ab0..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; } @@ -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; @@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list { padding-top: 5px; padding-bottom: 5px; } - +span.missing_ue_ects { + color: red; + font-weight: bold; +} li.module_malus span.formation_module_tit { color: red; font-weight: bold; @@ -1703,8 +1706,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 +1767,11 @@ ul.notes_module_list { font-style: normal; } +div.ue_list_tit_sem { + font-size: 120%; + font-weight: bold; +} + .notes_ue_list a.stdlink { color: #001084; text-decoration: underline; diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png index fb0467ae6..5c710a76a 100644 Binary files a/app/static/icons/scologo_img.png and b/app/static/icons/scologo_img.png differ 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/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 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) 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 %} 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 diff --git a/sco_version.py b/sco_version.py index 1bdc8323b..b47a266d7 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.66" SCONAME = "ScoDoc"