From 0e42df55c927bde01949984d4c20a741db03e834 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 22 Feb 2022 18:44:53 +0100 Subject: [PATCH 01/26] =?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 60c6f2a009..25111f7e6b 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 b639d5ee40..aab148e707 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 02/26] 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 59372b8d62..f6ca2ff299 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 03/26] =?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 5f652ec5ce..92b0ee5ffa 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 04/26] 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 c489510b56..46e0c066af 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 809d8e4631..d1c842dd48 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 05/26] 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 92b0ee5ffa..f132012627 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 4ac94b2109..5c1b87f2e5 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 06/26] 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 bbcf4a0837..db775438c9 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 07/26] 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 de7f28c9d7..ca6748de74 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 08/26] 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 7be1064441..cd211ca05c 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 5c1b87f2e5..1bdc8323b8 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 09/26] =?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 2be78713d6..c89983271e 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 1cc6e89c0a..b3d99ac3e9 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 adf70171b6..c4083a53c1 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 d3fbdcf876..e1aa9e3c50 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 10/26] 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 46e0c066af..02272ce878 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 11/26] 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 0e09968003..3949f50a9a 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 1fb98e3d38..2e8cdaf664 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 1bdc8323b8..7eaec052d4 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 12/26] 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 157cb6e594..4bc039baa2 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 7eaec052d4..b47a266d71 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 13/26] 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 e7710a910d..b1482ec5b7 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 14/26] 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 2bedc0c11e..9ea4e2fe78 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 15/26] 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 0a95621e8f..18c57beb02 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 16/26] =?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 061bf43c66..7124d3c30f 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 17/26] 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 7124d3c30f..8f9488557c 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 ecd8b0ab0c..b74eba0e31 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 18/26] =?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 f132012627..7fa75c1b77 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 700dec26ec..0aa74ef4b7 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 ca6748de74..a5d84cf4f2 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 f6ca2ff299..bcd6522b15 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 2c84651501..4bfa7b725c 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 987dc962f9..c83e1cc4d0 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 d6e244e791..fbf190c52a 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 b74eba0e31..bd99c80c6d 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 zcmV-tFqqHMr~fP?@5`Tzg`fam}Kbua(`>RI+y?e7jT@q zQ9J+u0EA^&NoGw=04e|g00;mD0TKWM00001009610U7`Wldu6nEd!td00RI300962 z005u@00aO4009610ED0e00aO4009610Am0E005`R^R%-P0(=2~FEvR-K~#7F?VSm* z9oJdszti1!zi)p}wybAK-fekPunh?bi4t$w9q`C77>a}{k{V`UpoU5UGhn%DYQi?5 zYA9fanyCV&punC~5(r705L-#SB#vdWS+ZlvmgJ}H?aO_4>*dVv>wC{r>sqoUS>EJ# zq<-gg_vzE!=ll16x1H~tYwo$n`jV!5W$V_hR*QFc|J-$#SfgcQ(&Jh@on&pNnUlTN zyu|yR?0r`2J--LcJr+RRd+dp=Tg7HUp=VErNG||{6lcfY^_aaBfTwfMV_I3=IprQO z4_F%F(H7U>vW1TV9@H-WJ;J+aX3bElc`G zWczke;+ai%b@KF+JxSJo95AG9Y4apkK3?9baqM41d){%{Jhs;A-Wllu^VnlHr!AFA zDWTFv8=J%~!5w5f_<&QS5`{l1N~mzcFZKjPQ^LFC$}V@uQ@pvIZhqWjtK6cem(K96 zr|qR+*hE2pH&SV*n5msRy}m7CcI*&uOuy=_BKO{VZ~9el+t&GF6#&_-bM-NQvxIl$ zY~EXT``U<>-R2ry?%gJzmvnnmev>IFL9XkF6!2ii#At4p<*sh_oGZI)T%EL$?jJp-dc~@4k?oZ; zQ1+AqxYUR?+KudN+#=geFRgr(L+qp>xPnUx2Hj+%@PDI}_lh@J7H?^<_>%+TtpOZ6_)0Il zaEFIQl7hnN7_`w#Jl%K6m0hmKU43^fyYI@q*Jt-rZs88B=BM8Ss-y+_xf| zXgLMLIE4u|ms33Ai)qJVTanmtw)Vwh(M2tPB1M+26T3yNlTLEP?e2EkWyc-wCA6#O zuGf86c6s(#Z&~*>C99T5rz1}1uebe}t9GyKJqHqw96Drt0N@{jh=4-Nr8)5`vq!wX zf_PI!@sxAyRy_le;Fv9pHN*v+Zjv59?`;xNC5yWz%c@E*whx`3V_02GiEyA zSnK8B8U?rpsz=H>_L8&p5?Ncz9_!uDo^v2@c}nOPxL(`RslOh73Z=z# zlr4yk2OB%A!0#JTWY?lFx)!FU#GIo$Go2&8ijeN6pvVCV2actsS${Dn{^FGQDL4Q` z;gEQT4jsx8REd>M*?mXIZ0@er+?Dlww(Qz)*SYr7dg`-q2UhbF_kgD(nJFl!bLrAj zs!yos*aaXI6v`#Su`W2^5HZ|;LYs4d(0tj0Q@}X_@><}f067*;^XLDCxHBGfAj+6L+ z`4l06g>C_S8cUt8IIeO9mwKkNU!#q{UqY1hSwx7y zEjk1m9t0^k2LMVi&*lw(OKo0caFnabNy+T&tPCqio}U~z+!@_Z@7v9_e7x&i{odSN ztDHhXk@Qx0qwgQ>a2>hq^oAZY+u5dYSXJQ?SP}+h(o|*<1BGcX@JHF7hf@r~DVDs! zfxa#K>FO1AQ{|(B;Z#-WtfEUS`-m46F`n1g+1d>W+8&NjwpLSu9C^Fp2lXWhkTgC_ zBdZVSvt_}cW!v0CJfcW8y=AW=m9$L-hv2c2`q5i7IOaDSUZq@0@4bwu zDkzsINm3$L(N*aq*RLCd3=FJ%<=JP|vis7rxt5Q&=>r^pubw-u=T`L!Iw9N1+Bs?J zYT_MGZ0&^Cttgqr349OZW*ktUFM56#6!`!pbD-Ze>lh%)CF+P6B5P(F;-$dow(Ymr zM`1NhF)ixh$WrPfHUy3%iGqWBrGRkYP#r`Sk^cUECEW3z`FO-miQK)dmXB9QPiN1? zZ}f4H<8`io_L13U35219i^6DdjK)LgctxJ_)I{o#>VP8kLCkq0!$T`vLPdo!l~jDR z;07MtAZ#^#Peq3wm4Z?=y|)94wJjSZg*dUzB#4V~T$E%)fl))>t<-F#T1^!|bP*-s z37&g}xQ@trjVn_;ZduQ3a`oQOdPQnwbo%4y-_(A8po8wF&Vk7409FC_o zsvD>?s;fi>2I4pCD3BcXO1{@t?jT;s@hD95?7dTN(r&dal(9iBpXw4pB5^xtM%8>V zDj-gZT~Sh2=x72Q&ZbHCkN(MmAj>7P$2)C*bgt>Gqeqofyx~BReyvCQ5o(}RX7_a( z7h)qF?@<4Q4uT1;O^t<6PiBlKb7&0K4v}Wqkn*;Vjowa)Ta*&zrnQ9*5#b87))o~# z0K~U(n6t42He&PhIKA_^FqS+Xf_&XdF==_Fc3q0gk>nQ_Bsbq|1WiwTlv6HGxs5)5 zZ||11I`E3XtvYqH$F=OP({i?s9)Cl6Huc4iNzVgBr_-D+Av<>Lp!2SiRG%Q`QY{Z1 z%u>;y96~qM73vU8IRYjpoFX+feIEdXVdr?nxwzgAyh7eX*s(&V!Pa((<|I{n)jYpP^9pq=R6=#fT5~nFhwk+}S(=X%rNgmb&>ZK%txhH3HGm zf+GNo6bNAuM>)TpggM{j00?yw@f8?}A0#2e@qhu*pH>Ra)1IrL>{|;+!>R`20DzM#5PKS z0rQTr1){Qtvgzj{2I1fqk}x0_=M@xi3%CZD5Zi2u;1zXX&^K{kd@mS!>vZ0_D>B$Q zt3i<^u6yxbQ1sTx?x!BiXK+Pt#Wz9HbGoT=nC2Ro>k)J}Ffd?hH5^Y}@xhlKV1W8< z6ihfofapMffI~EY!+bL|1un+35ttSn11_QMxn!xUtcRmPq0vcz3^p;G{aUgO&6 zD41PP6d*ExXfXaX`DjO%@Kl5-hd^}TVN|dLaPkxsVY`JXT2M}rtHoh{X4&)~nK$d| zp&2``-##@q;Lpv};=#!Uvp#`$tf|=j<-D!8!XQ^`#|2{b!fK9@4-bvJ z5mEs(=oU#KFBXePhf?srx8N3dGdfB0^s7g-SVW6|de1Bz%~q=`+`!e8?@i<&)K=@x*4st z%|NweC&wyw%D3pLsi_*GaOM(KI?K8Aix&mPLR|{WP061}R3E@-=(EB&><z4fyD*|Ij0ijGZ3mCO@= z0)o0JFrY+$X{e$jVE8jtGyw+6X5Me&z$)UE+%qj}pO{E)YeeRfk+Oa8%JaOhA6$_2 zd#CLORScceXSc5%NS;`12GyD6WFx-M8lc!yLI+V`pj<}vw&d_`w%7Ef#7{x7e^&Y? zmt}agB#T#_Cx>|^C9Zk+85w(SUd~^CHz51oc9A@*$KV!ThFfqbO%5QAQ>db;<=18E zCFU_N@dBXey)L`&%9{6X3V`h929$hsi5%vkS{`^a(=XEKxQBW)W5fz>@fd|O_vl`K z%Vf>Gmk_+Ei-wcW1Q2-7r1?-Yv=;rIx*8W+uDT<{yT=jNUOTvJEtll_k zA&!a!0mmChf*Iu&$|)vir1Io+d?8*|xoWLEH9nMz3;XRoa?X!tWOLq==?`p@M{$Wa zi5d-9Zz&-%U3CYS94H*8c-?}+5zoEY^YRQRvURg%y(YWgTh@KIWx?Re4ipLo)%gHK z+s`xLsd64rcc3{w4H%!YG-tR(cjdT@1Bti!meBD94 zCHaCM#nx6nqT zoKIQ5?Jszf*2K|LL&ipb`s~50#*^m&TrgJ7H@~xA*6*K}x8VJ(UA9)f3CGw!xgdS_ zJt^;QMsm?WA^w*;uJ<3&_vo6@$+r(kxi2r#rm-}7RNqUvhbpVDJ5Hg*IqB2`ir#Cp z`^PKmG2OJodbbroa4t1NwT|irSy0rY5T8ZPTU?e>t&IWUNqhHyw72QNqP+v(WpSip zzf{S~F<-~PhKE@v+U~oye>hJB^>W?atUB4K{aKs&z6sO&Yjn5wVVZocA$!RxI>z& zw(sLFz*j{*qOJJc((53qIM>x8II8h7L zmqsY(^E?;e)7Z0ly?p-ib7b-#9+O)M?Yo^Io@XyzBmd{7%TumXIE=!%{b|`y@ucyA zYh_>eJsc?7uNRM|66Um0hFfIYc(JmccUu-14iE|o-E{vLP_)r240?s?6i9H7NE!nR z&+VCv&s_?C{eg&4@!7h$9#FiQ8_bk)_7UPIa`VgPU5t;f?JwFL=M2a{&ezP~eABzh zGdQuBSx6{ou9Q!n@IgpD_RcSQ$w8EW01bgiJ+A~niYT;ec-Wfc@t^*}PXyo6=YDol@((>DSKth5{M>baY51v!>t67`z~~4aeI1OFW(mkM zpL*&7Ly4;?+g`SuJ*UUr=0M>9;c^9qo=Z2B&D6cE;~spvDJY1{Nfl0biajtZL;I%X z!lkARG}_6AC^5?J3(O|`E+XPE-p=cm+x8k&I7ch-=fcF!&((vQ5J?3_(WjZG-mju0 z4;bKo3KuijJ_u`K-f}XZSnoW>)GK+PAz0rsIB)JF_Tr(rWw{DKY^DBi-ZOvkvA4@- zc-{n1l)k!0-okS>Kl~PXm}6?P7-pp@C|ZCbYn|xn*Hr2y+G#&rSByA9YdgJdXZKw_ z&6Vg1gp@0gI}VXOmU0Vx6rST0)H7Z2q1W4g8Zno<4#pQwF8Y@&x0Cgpz7*4Rl&ONx z`JR0$j_tZeXs%~Qh$))ZgM{Tg(Xgd!o2}Lcl+Z~D^8GZlSPJYD%y?hlO3W32Vm2^# zAMbXqHQsvmbxLE7>_KDWF9V42zz^OJr@AnSlE1y}TJKLcjLPE9De3$AF1Z!mrS&U+ zKP6w{`4S!E>vD?MG$^t};<;a1>JcN3=&sT1y|U*VAT)Pn2L=a{G{*vAw3H8D>oMmP ze+9G@)IdC(AOIvK&&DSzRbbD}@K>8kt$pr}0XL~^?^Ui;3?^8c8DrdEeOyWQ|wzMOA1rDeozHG)oWp6u-7nu!o2~$AMV;tp3Q5uh+M&y_tbZs0qBj%}^ z#1ux14R;rE!8{>aZ;leF5dOC-=i9|%p>QQL%m#{ioFm3N*b1Z1*BUK5P$~Wd;TxAX z+x9zas^*Vwyi$HRM{x1iewh4!GT|W2+pd$p8t+fRpzolAXR1@Y=0K4p5YKpKQwA8? z37WF^)3P3S$J~BObdEa^^yVr)I(3);hWAQM)3goc7|i=f@0CMI{N90R{OOtC-Ahe- zBbLHijgMeFZn^HCI2yYYG&O7(Cm{mjH$%a!M@j&vw5HEY`0>cs4-7gD>Z21`CQ`udoby7g^#gZ2d!{-KcFn>4&m$Zqb(Vz#N zC8jrF23Yj?0ol0wNOET*HXnq*{e1;9$wZNvS~DJ6Zboa6_0Yy1%$fEqTw@p=wak!5 zt=W$H>mlo><>kqLN+Ce6Fc-AjQJ5rgGoK3z`5Yr~eC0YVElRw@%`jd-v@li8?+asf zW}46T6{Guxi^0^hbIC^$GqX2eDu0GKYsZ+y$lBow3M}2%1}L<1U-1Mm9C1^^W*)%p zJ7Bo|6XmX+VZ=1mB?xd?c_#pq+7zsD;g9r5$jD~#(Y?uktqTqFQ5w90#aCzfqNm%T zEicxiG4u_M>S4{aXr84F$`TZ+0H=nIP@Jhrf8cw2`YTKmux10gNWh!`feV--qhI7P zkiyuIWt>(#PD0$LmyctQp%eQ+Ip_UzxgZl$HTfk^k|RHLzV|;Dc&;l(Wa*r9Qcz%y zzWSh8>AVboQC^W^M!#_2aQD?`QUK_tX@%&N9WY!SsS~sd=?pWtQv05HIv(g?H3Il? zg9(2ckWXTf5*RmwPy^E5zB9UPV$u6|$o>zNbJkRg<^UhKx73VBmm4uYDpN-aEEau2 zGlO8DOwa{W0fHk7VSr9B-B-c+VZHu#l;k6P=72$eaD_`gU^2A&iFw zNxV3;=DV1o|AcO_6A$XobIG$;tdqZ)Uy#BLKQ4zhrb?y`n7z87(2jjo(+Pm#2(Ec{ z(`U`@r!fk97p2I^XMWI^_C@D)VjVR2W}3Ar%1qi>1|6^uU5~ZEoa_xxR%Xmos zm)|PS@NNXyq_6m@DVpi+^$}j}bR1x037_SE7W$~!bLsw%CZx1yQ3f<1cYReBH*ZWM zoh)MvxduxKT(Bn9>gC~QW#G`fjP(UlyX9&*@b}+|u0FWne}XUhgXLTjmUCu~i3pWu zD=M^@7}Ab?;Dk&GY?aVFKZlnK&L98>xzA%UYY4)7dQ1(55^k#;-Ymom)@>?6Vn9KE z7^s3W1;rx3xR28Pw0(sRI7|3evmt|9A~{ui9rFB3OjE{8w+xU5?W<=o*yYvTQz3kSD9DPw!) z;>{SL<#Pw*UoYDz(_h(<{6f8LKG|1)F>9E5zJxyD*Zc^^B}%2>ViGbqqJyU&oomxWhG6T3**LUw_kua_EuWa>HCbc?WEI`n;<5mBn^6 zJhc@3Hc?dX#s83?E4COU!-WyaLesRD8a68UMB}k&0U_czE>RB=7mST4D3A}Vq|~;! z)DuBhuw@k?JUu59tP>VzLj&P|^QSsw!Cu)bjW>Xg!PWe=jIBt=Bepl9970hV&VX1!oS-EDpk!EAO|2aJiwD>d?C2QbzN{}D&ZGwp|EbYCLd!wVjSB2BU61)3+yiu zaI+cWE3Z$Afl@1um)j=BRAtq@Bn1T?OPyw*a}1d{gkV4{Fp#Dq1oyU;O2Jqi{{TGc zioOuvB0~@zDoRWasWaJV%MZw5ILX5>m_cCk_yr68-`{Zuy67u^C_Y|SfN&H)4ltBs zxU}+4`xlr)%F4Ur<7sGH`=5Ux8~4o0`-rKy0d7&p<4DlGT)-HIM&f*7?T|c*s`w6E z-M<1WZxVI2iwWo#mGW{eA^_vo5Xv9h0Di!9LtO_PG~x|M*uyMzbql0}S`>f-6@cl< z(gu>s77!h_l^t0llxO;vkKZDH&pEDnWps*_ z&U3x_ddj=NP%yY5^DHogA!q-wNvUAg6$yfE-h5fQo`3sqAC~KnH02`G_NLop6^YW$FH6XIx0ytEJIAEl`%f5D=`S!C?A)J0JZb>y4v={IEiToap zhkvm>{$MSC@_&O*`eIl*16Q`j7dpUK{R|CkA1q5h3g^%A6<@K~ux*`D)}Ob1)J3bX z2~qqOE0n~IpeO`RqhG*8N4HR_LqjFJ0z)fmT{x4)A%ZlKc`OZvf zkL?8GF;8NlV64hte*6~mMZL$%ZF!tEfPlbhqSJbRfuYA$$@IRsOGfuk$whF6VU{;L z@}bM*yNBu$+`lLJFe2bT@bwQP8uk>JOGbQy-wtIb$DXL8z;G|HN}ce3bz>y5I^OR}(Z>F_J1g3J}`0STgC_N*n|fxGDh!brJ;y z93o(UIcanYv#%A|g&=4C@V9O>-`Ixbwe4k$x;pCsk)nb>0HarwK!$Ygg#UNnmhsu9 zY({xIpRk7g7o8&yP0dT?$a3=QSOOp63l0WKW{+AJNcRv$b0CFqC~x1Fb?D6 zn^;l6)7kHmnyR<*MKX)KT=l4QjaP=(seS`#o=L=wiu9`M|3^F z)Pqm{#!cqP%LR(F9uV|#npFN$I36&}Up*oh&o|`#5aD`gw1=ewz5yXOLgbH?isnr) zfF|P)(})V&jBTC4sl!NuJiyq9mM~0zyGl=JtM`DCN_DW20swjwzNc0Q-b+LYSa6`w z2UAd>Cunu$7HLE29?bwa@W&z;ppT%NWNw)sCrHd+moJdt`}Ipq>*dfX-Y|gB&ezlh zhW^%=&xB-eVq$_m7?+*=7U|Oq!jXL4o(OQ;*{3{;Da7sl7%B7+yg3Bc6Fa&TAADWo|`iWc3|KMWarLCSh%Oh=0n|vcC2abDpNOfw7 zA0PDEjFq!MRVW*XfVmYr;4+wh_#Oyy5DH$8E^s696GY0{dVf(zG#N5)SVp$hs!+d^ z(vj79A(YyY`qk@lzG`l2Qc$QK!8ivCc4ComP8Cp1dK@J(6(LwV9D$pFFqBQ2LV0C0 zj8WGJP~>K6W*T$%S3mh~^Z9%3vA#O5(nij}pEoohvi)}teLl5a#xN~^FQ!w@!LTeK zHr7Fl8=&_PR_0!4L1LC|e7Y=MW`7KpumQ^>#$naw zYxeMLdVIP3t{8lVC7u-nSd!|GPB{bMO7EZ+h&r@rFI70ZcmQvzJ#;}q?z5triV|Gx z9A*tZ+!lxq9Un=+VE)Lw{bn4S+kfZ1W@;;(;@&gBDOLfIlBoNC2jW2!QisQHd%zY; zEk;Ovc{>NMhgusT4C|M%FcKrARMlmSu3+?YXn>`Qbc9o_0?gEu#%h+T@u1>>aJ$qL z)v^MEdg^yb@0KnC4t$hhaBt45qS+B6Iv%2R)rkeaP*G!1Lrj0kkLxV=dw5>{Z~I&F z;ceSgO1*T)m3sDnR+H-iC+z9iZFh^=MqC1?9@XtrU$k%X{p5Xo);|R>E`o65kah|Z zFkwg5;N4sgAd0L%k>JTB>}#h01OmOmf*t%qB^(h zlob?=o)L^e1P-WB;H2|LG#tam{y^D_Mk?kPXWn9Va?$U9->nz&P;952QcRxq)o1&K z3Jf}VTgEf^-~Pp3%Hr$q<~x2LpX{}CAc6fd32UkIWskttNBfErR1hlakm8u?hAAGD zRH|2WM21bdfN}#>K$UZV8nr-Fl%z?|sfQEi6r-moB^)K%d!=4E@5O}eF`>k-vrNH< z5gngROphfFqVaRg5K?E<0 zWichaRLI#NAHe4ppr)7s#6MAI7q_>!R&I! z6*#S&Lph3?tJGyY9=UX6dO_wE7NyM6yk)h4t7Tg)SHfyJ&(8yx&+Xi8?)=M-DYsJa zb-=5CZJlgd1;okrR8KwS7;=}$wyrq&^gXr|4GQ{2!VEF<9=`A~E?IiD3^dE7=l{9`-xlq|H#1BJNarq z4W;>cTNx*&!b~sQS%NP3r3h-WAto#_8U)U$Nn$KHi$!uHL!{(k{GIL$*>*~fFPa|3k4mc)lPo6d%`X( z%)@YusS>;~fL+^<8B=8uZ;i-+QQ+M2YzwoeZK{D^=>IFY)JHncuA=D0t&UYdyc9~G z;$h+xg+0yj~3%xZe~IL=0DwReyQ`^FU}=a0dXcMe41_OS9|47u};VM z^!@fq#zlS}*8kIZu-0QdB>YGWKRCw%oqYU<=bGN`=O)d}%q&0tl9ztw77Pvc$r_g1 z9b&~)ejp+Y9#2n7v*k@B6Lynw<(nC$>)X$g6{YTPNz7SKzo>CHgbk zN~pWAG*AtmVD~@&!Tt7c7_#^`wYI$tN7E&`HWmRmtxBbsTX*gnok3`3XXc5}iB$o` z8o?#Xui0sso^Eg(sj;FCrfh^tF@*HEns#?Saj)toFPvVq!V6!%diG5AKn56)ROG+k zS3f^8rT0N&UD#xQGZJB>j*^L&GC6p7(#$U``6ELEtdu)|9|GqTe58O5f#^uX@4!vC z+!B`3GJo>PTg~sQuY{DgeWK@H1;mMZ=2bkdz{r}b+dgXtOBH!LfcPMkCO4Px6H+oa zvzRO_E!)0I$@JmI)}?ke3X6KFuM6E{31^pvUHtD)++u!zZxs-)B2mx8t<}qz9WPN= z^qzrmGX~}d-~xB>lh#)-%pr>lbt0+!Fo#@Wumm3`(?vi7qon-UsrfQeVjr@mlW7BT*rAm@LS1!`_!VJo8b>|vZ#x~^3jK;)4P-)TqffB(Dh zE$$K8!0q*ag5pA7bX8=$>f}C!2Y{&Cx-@gv!moYN7RCd48-A(LuodZVbr8TC%pf`H z=$VH;d9ztKSumW!ldM*Kt@VI%4PriD)JoKPr}FHlRQXgIUakAe^neolUPf8RncQV6_KUdtkLH2UdGvwFg#vV6_KUdtkK(-k3e` Z{{d1ufCu*9Zr}g_002ovPDHLkV1jOzoFxDN diff --git a/app/views/notes.py b/app/views/notes.py index cd211ca05c..a8c387e4b4 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 19/26] 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 5caa3d3934..6a44dbc00c 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 1d01f4f42d..f4cdda8ab0 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 20/26] 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 b11f63ca86..f9521b2923 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 21/26] 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 6a44dbc00c..2aec3b7366 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 7fa75c1b77..6a821ac69b 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 09469fb051..2bed88a383 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 8f9488557c..73e77dbbdd 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 bd99c80c6d..3b80f7d6df 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 0cd3e333b3..98c590f7b9 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 22/26] 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 2aec3b7366..db42616c8e 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 6a821ac69b..7d2eb5ae32 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 23a8b8340c..653cdb80dc 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 73e77dbbdd..8cc3269a1c 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 3b80f7d6df..b3a80640aa 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 23/26] 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 f4cdda8ab0..20e63cba01 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 7d2eb5ae32..8fa106f504 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 24/26] 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 18c57beb02..99738336ee 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 25/26] 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 8ebf5deabc..220ece566f 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 26/26] =?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 0000000000..5ded75245a --- /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