From f7db75e1a2b8358445e74f1a5668ea9c3b61a354 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 17 Jan 2022 22:32:44 +0100 Subject: [PATCH 01/70] Fix: exception si import notes sur etuds non inscrit --- app/scodoc/sco_exceptions.py | 10 +++++----- app/scodoc/sco_saisie_notes.py | 6 +++--- sco_version.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index d975766e..dd872a7a 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -36,11 +36,6 @@ class ScoException(Exception): pass -class NoteProcessError(ScoException): - "misc errors in process" - pass - - class InvalidEtudId(NoteProcessError): pass @@ -56,6 +51,11 @@ class ScoValueError(ScoException): self.dest_url = dest_url +class NoteProcessError(ScoValueError): + "Valeurs notes invalides" + pass + + class ScoFormatError(ScoValueError): pass diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 0fa105db..d0a5407d 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -487,10 +487,10 @@ def notes_add( } for (etudid, value) in notes: if check_inscription and (etudid not in inscrits): - raise NoteProcessError("etudiant non inscrit dans ce module") - if not ((value is None) or (type(value) == type(1.0))): + raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module") + if (value is not None) and not isinstance(value, float): raise NoteProcessError( - "etudiant %s: valeur de note invalide (%s)" % (etudid, value) + f"etudiant {etudid}: valeur de note invalide ({value})" ) # Recherche notes existantes notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) diff --git a/sco_version.py b/sco_version.py index cab845d1..23676c26 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.25" +SCOVERSION = "9.1.26" SCONAME = "ScoDoc" From 30e7fd516b254df657dc2c0a4c0d323db0115f5f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jan 2022 20:23:47 +0100 Subject: [PATCH 02/70] exceptions decl. --- app/scodoc/sco_exceptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index dd872a7a..5f64f57b 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -36,10 +36,6 @@ class ScoException(Exception): pass -class InvalidEtudId(NoteProcessError): - pass - - class InvalidNoteValue(ScoException): pass @@ -56,6 +52,10 @@ class NoteProcessError(ScoValueError): pass +class InvalidEtudId(NoteProcessError): + pass + + class ScoFormatError(ScoValueError): pass From 01dcd8cccda5d953f462f67a11b60f2527739ef1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jan 2022 21:38:00 +0100 Subject: [PATCH 03/70] =?UTF-8?q?Migration:=20tol=C3=A8re=20dates=20logs?= =?UTF-8?q?=20aberrantes,=20et=20=C3=A9limine=20relations=20manquantes=20d?= =?UTF-8?q?ans=20entreprises?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scodoc.py | 30 +++++++++++++++++++++++------- tools/import_scodoc7_dept.py | 8 +++++++- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/scodoc.py b/scodoc.py index 03f656a7..976443a3 100755 --- a/scodoc.py +++ b/scodoc.py @@ -278,20 +278,36 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name= db.session.commit() +def abort_if_false(ctx, param, value): + if not value: + ctx.abort() + + @app.cli.command() +@click.option( + "--yes", + is_flag=True, + callback=abort_if_false, + expose_value=False, + prompt=f"""Attention: Cela va effacer toutes les données du département + (étudiants, notes, formations, etc) + Voulez-vous vraiment continuer ? + """, +) @click.argument("dept") def delete_dept(dept): # delete-dept """Delete existing departement""" from app.scodoc import notesdb as ndb from app.scodoc import sco_dept - click.confirm( - f"""Attention: Cela va effacer toutes les données du département {dept} - (étudiants, notes, formations, etc) - Voulez-vous vraiment continuer ? - """, - abort=True, - ) + if False: + click.confirm( + f"""Attention: Cela va effacer toutes les données du département {dept} + (étudiants, notes, formations, etc) + Voulez-vous vraiment continuer ? + """, + abort=True, + ) db.reflect() ndb.open_db_connection() d = models.Departement.query.filter_by(acronym=dept).first() diff --git a/tools/import_scodoc7_dept.py b/tools/import_scodoc7_dept.py index 597b9baf..75f5e9a3 100644 --- a/tools/import_scodoc7_dept.py +++ b/tools/import_scodoc7_dept.py @@ -170,6 +170,11 @@ def import_scodoc7_dept(dept_id: str, dept_db_uri=None): logging.info(f"connecting to database {dept_db_uri}") cnx = psycopg2.connect(dept_db_uri) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + # FIX : des dates aberrantes (dans le futur) peuvent tenir en SQL mais pas en Python + cursor.execute( + """UPDATE scolar_events SET event_date='2021-09-30' WHERE event_date > '2200-01-01'""" + ) + cnx.commit() # Create dept: dept = models.Departement(acronym=dept_id, description="migré de ScoDoc7") db.session.add(dept) @@ -374,6 +379,8 @@ def convert_object( new_ref = id_from_scodoc7[old_ref] elif (not is_table) and table_name in { "scolog", + "entreprise_correspondant", + "entreprise_contact", "etud_annotations", "notes_notes_log", "scolar_news", @@ -389,7 +396,6 @@ def convert_object( new_ref = None elif is_table and table_name in { "notes_semset_formsemestre", - "entreprise_contact", }: # pour anciennes installs où des relations n'avait pas été déclarées clés étrangères # eg: notes_semset_formsemestre.semset_id n'était pas une clé From b53969dbddd964878cea7c2858cafca68ce2ea4a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jan 2022 22:01:30 +0100 Subject: [PATCH 04/70] =?UTF-8?q?Option=20pour=20faire=20passer=20les=20?= =?UTF-8?q?=C3=A9tudiants=20m=C3=AAme=20sans=20d=C3=A9cision=20de=20jury?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_inscr_passage.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index ab27ecda..7a60b67a 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -49,9 +49,11 @@ from app.scodoc import sco_etud from app.scodoc.sco_exceptions import ScoValueError -def list_authorized_etuds_by_sem(sem, delai=274): +def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False): """Liste des etudiants autorisés à s'inscrire dans sem. delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible. + ignore_jury: si vrai, considère tous les étudiants comem autorisés, même + s'ils n'ont pas de décision de jury. """ src_sems = list_source_sems(sem, delai=delai) inscrits = list_inscrits(sem["formsemestre_id"]) @@ -59,7 +61,12 @@ def list_authorized_etuds_by_sem(sem, delai=274): candidats = {} # etudid : etud (tous les etudiants candidats) nb = 0 # debug for src in src_sems: - liste = list_etuds_from_sem(src, sem) + if ignore_jury: + # liste de tous les inscrits au semestre (sans dems) + liste = list_inscrits(src["formsemestre_id"]).values() + else: + # liste des étudiants autorisés par le jury à s'inscrire ici + liste = list_etuds_from_sem(src, sem) liste_filtree = [] for e in liste: # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src @@ -125,7 +132,7 @@ def list_inscrits(formsemestre_id, with_dems=False): return inscr -def list_etuds_from_sem(src, dst): +def list_etuds_from_sem(src, dst) -> list[dict]: """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" target = dst["semestre_id"] dpv = sco_pvjury.dict_pvjury(src["formsemestre_id"]) @@ -224,7 +231,7 @@ def do_desinscrit(sem, etudids): ) -def list_source_sems(sem, delai=None): +def list_source_sems(sem, delai=None) -> list[dict]: """Liste des semestres sources sem est le semestre destination """ @@ -265,6 +272,7 @@ def formsemestre_inscr_passage( inscrit_groupes=False, submitted=False, dialog_confirmed=False, + ignore_jury=False, ): """Form. pour inscription des etudiants d'un semestre dans un autre (donné par formsemestre_id). @@ -280,6 +288,7 @@ def formsemestre_inscr_passage( """ inscrit_groupes = int(inscrit_groupes) + ignore_jury = int(ignore_jury) sem = sco_formsemestre.get_formsemestre(formsemestre_id) # -- check lock if not sem["etat"]: @@ -295,7 +304,9 @@ def formsemestre_inscr_passage( elif etuds and isinstance(etuds[0], str): etuds = [int(x) for x in etuds] - auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem) + auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem( + sem, ignore_jury=ignore_jury + ) etuds_set = set(etuds) candidats_set = set(candidats) inscrits_set = set(inscrits) @@ -323,6 +334,7 @@ def formsemestre_inscr_passage( candidats_non_inscrits, inscrits_ailleurs, inscrit_groupes=inscrit_groupes, + ignore_jury=ignore_jury, ) else: if not dialog_confirmed: @@ -411,18 +423,23 @@ def build_page( candidats_non_inscrits, inscrits_ailleurs, inscrit_groupes=False, + ignore_jury=False, ): inscrit_groupes = int(inscrit_groupes) + ignore_jury = int(ignore_jury) if inscrit_groupes: inscrit_groupes_checked = " checked" else: inscrit_groupes_checked = "" - + if ignore_jury: + ignore_jury_checked = " checked" + else: + ignore_jury_checked = "" H = [ html_sco_header.html_sem_header( "Passages dans le semestre", sem, with_page_header=False ), - """
""" % request.base_url, + """""" % request.base_url, """  aide @@ -430,6 +447,8 @@ def build_page( % sem, # " """inscrire aux mêmes groupes""" % inscrit_groupes_checked, + """inclure tous les étudiants (même sans décision de jury)""" + % ignore_jury_checked, """
Actuellement %s inscrits et %d candidats supplémentaires
""" From 0e6d5701688cfce13d668835f2f05390e7a0e279 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jan 2022 22:55:33 +0100 Subject: [PATCH 05/70] =?UTF-8?q?oubli=20dans=20le=20commit=20pr=C3=A9c?= =?UTF-8?q?=C3=A9dent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_inscr_passage.py | 1 + sco_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 7a60b67a..8b0f6cbc 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -375,6 +375,7 @@ def formsemestre_inscr_passage( "formsemestre_id": formsemestre_id, "etuds": ",".join([str(x) for x in etuds]), "inscrit_groupes": inscrit_groupes, + "ignore_jury": ignore_jury, "submitted": 1, }, ) diff --git a/sco_version.py b/sco_version.py index 23676c26..88ff029d 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.26" +SCOVERSION = "9.1.27" SCONAME = "ScoDoc" From f524dcaf589d74fc5ca6fe95b53917ce85ae38b4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jan 2022 22:40:41 +0100 Subject: [PATCH 06/70] Variable SCODOC_MAIL_FROM pour les envois d'exceptions --- app/__init__.py | 2 +- app/auth/email.py | 2 +- app/scodoc/sco_preferences.py | 7 ++++--- config.py | 3 +++ sco_version.py | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 04e28207..258720ac 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -249,7 +249,7 @@ def create_app(config_class=DevConfig): host_name = socket.gethostname() mail_handler = ScoSMTPHandler( mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]), - fromaddr="no-reply@" + app.config["MAIL_SERVER"], + fromaddr=app.config["SCODOC_MAIL_FROM"], toaddrs=["exception@scodoc.org"], subject="ScoDoc Exception", # unused see ScoSMTPHandler credentials=auth, diff --git a/app/auth/email.py b/app/auth/email.py index 9ac8a017..61759691 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -8,7 +8,7 @@ def send_password_reset_email(user): token = user.get_reset_password_token() send_email( "[ScoDoc] Réinitialisation de votre mot de passe", - sender=current_app.config["ADMINS"][0], + sender=current_app.config["SCODOC_MAIL_FROM"], recipients=[user.email], text_body=render_template("email/reset_password.txt", user=user, token=token), html_body=render_template("email/reset_password.html", user=user, token=token), diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index b38fb736..77242d65 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -111,8 +111,9 @@ get_base_preferences(formsemestre_id) """ import flask -from flask import g, url_for, request -from flask_login import current_user +from flask import g, request, current_app + +# from flask_login import current_user from app.models import Departement from app.scodoc import sco_cache @@ -1537,7 +1538,7 @@ class BasePreferences(object): ( "email_from_addr", { - "initvalue": "noreply@scodoc.example.com", + "initvalue": current_app.config["SCODOC_MAIL_FROM"], "title": "adresse mail origine", "size": 40, "explanation": "adresse expéditeur pour les envois par mails (bulletins)", diff --git a/config.py b/config.py index ae507b1e..fca2fc51 100755 --- a/config.py +++ b/config.py @@ -26,6 +26,9 @@ class Config: SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin" ADMINS = [SCODOC_ADMIN_MAIL] SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL") + # Le "from" des mails émis. Attention: peut être remplacée par la préférence email_from_addr: + SCODOC_MAIL_FROM = os.environ.get("SCODOC_MAIL_FROM") or ("no-reply@" + MAIL_SERVER) + BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL") SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc") SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data") diff --git a/sco_version.py b/sco_version.py index 88ff029d..5ddad619 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.27" +SCOVERSION = "9.1.28" SCONAME = "ScoDoc" From 908f78477dd0a0399251db6dbd1f353f81d2a523 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jan 2022 23:02:15 +0100 Subject: [PATCH 07/70] =?UTF-8?q?Augmente=20taille=20max=20code=20Apog?= =?UTF-8?q?=C3=A9e=20+=20check=20edit=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/__init__.py | 2 +- app/scodoc/sco_edit_module.py | 9 +- app/scodoc/sco_edit_ue.py | 3 +- ...874ed6af64_augmente_taille_codes_apogee.py | 84 +++++++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 migrations/versions/28874ed6af64_augmente_taille_codes_apogee.py diff --git a/app/models/__init__.py b/app/models/__init__.py index 0fee7bc4..4a3328b3 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,7 +6,7 @@ XXX version préliminaire ScoDoc8 #sco8 sans département CODE_STR_LEN = 16 # chaine pour les codes SHORT_STR_LEN = 32 # courtes chaine, eg acronymes -APO_CODE_STR_LEN = 24 # nb de car max d'un code Apogée +APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs) GROUPNAME_STR_LEN = 64 from app.models.raw_sql_init import create_database_functions diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 813320d5..25ec3d25 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -32,15 +32,15 @@ import flask from flask import url_for, render_template from flask import g, request from flask_login import current_user + +from app import log +from app import models from app.models import APO_CODE_STR_LEN -from app.models import Matiere, Module, UniteEns +from app.models import Formation, Matiere, Module, UniteEns import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType -from app import log -from app import models -from app.models import Formation from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( @@ -294,6 +294,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): "title": "Code Apogée", "size": 25, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), ( diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index a52826b7..c9a2da17 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -1216,7 +1216,8 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): def edit_ue_set_code_apogee(id=None, value=None): "set UE code apogee" ue_id = id - value = value.strip("-_ \t") + value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque + log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value)) ues = ue_list(args={"ue_id": ue_id}) diff --git a/migrations/versions/28874ed6af64_augmente_taille_codes_apogee.py b/migrations/versions/28874ed6af64_augmente_taille_codes_apogee.py new file mode 100644 index 00000000..7ac8c8c3 --- /dev/null +++ b/migrations/versions/28874ed6af64_augmente_taille_codes_apogee.py @@ -0,0 +1,84 @@ +"""augmente taille codes Apogée + +Revision ID: 28874ed6af64 +Revises: f40fbaf5831c +Create Date: 2022-01-19 22:57:59.678313 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "28874ed6af64" +down_revision = "f40fbaf5831c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + + op.alter_column( + "notes_formsemestre_etapes", + "etape_apo", + existing_type=sa.VARCHAR(length=24), + type_=sa.String(length=512), + existing_nullable=True, + ) + op.alter_column( + "notes_formsemestre_inscription", + "etape", + existing_type=sa.VARCHAR(length=24), + type_=sa.String(length=512), + existing_nullable=True, + ) + op.alter_column( + "notes_modules", + "code_apogee", + existing_type=sa.VARCHAR(length=24), + type_=sa.String(length=512), + existing_nullable=True, + ) + op.alter_column( + "notes_ue", + "code_apogee", + existing_type=sa.VARCHAR(length=24), + type_=sa.String(length=512), + existing_nullable=True, + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "notes_ue", + "code_apogee", + existing_type=sa.String(length=512), + type_=sa.VARCHAR(length=24), + existing_nullable=True, + ) + op.alter_column( + "notes_modules", + "code_apogee", + existing_type=sa.String(length=512), + type_=sa.VARCHAR(length=24), + existing_nullable=True, + ) + op.alter_column( + "notes_formsemestre_inscription", + "etape", + existing_type=sa.String(length=512), + type_=sa.VARCHAR(length=24), + existing_nullable=True, + ) + op.alter_column( + "notes_formsemestre_etapes", + "etape_apo", + existing_type=sa.String(length=512), + type_=sa.VARCHAR(length=24), + existing_nullable=True, + ) + + # ### end Alembic commands ### From e5fb9f4365617d207bd20cbc3068f098fdf777c2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jan 2022 23:56:32 +0100 Subject: [PATCH 08/70] Fix: ancien mode de calcul nt / cmp py3 --- app/scodoc/notes_table.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 996fedda..2209aea7 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -814,7 +814,12 @@ class NotesTable: moy_ue_cap = ue_cap["moy"] mu["was_capitalized"] = True event_date = event_date or ue_cap["event_date"] - if (moy_ue_cap != "NA") and (moy_ue_cap > max_moy_ue): + if ( + (moy_ue_cap != "NA") + and isinstance(moy_ue_cap, float) + and isinstance(max_moy_ue, float) + and (moy_ue_cap > max_moy_ue) + ): # meilleure UE capitalisée event_date = ue_cap["event_date"] max_moy_ue = moy_ue_cap From fa5fcc8f571d274fb92d80b194a566edda899a62 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 20 Jan 2022 13:00:25 +0100 Subject: [PATCH 09/70] =?UTF-8?q?Edition=20modules:=20interdit=20changemen?= =?UTF-8?q?t=20de=20semestre=20si=20utilis=C3=A9s.=20+=20doc=20+=20bug=20p?= =?UTF-8?q?oids=20vers=20UE=20d'autres=20semestres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mod.py | 6 ++- app/models/evaluations.py | 11 ++++- app/models/formsemestre.py | 2 +- app/scodoc/sco_edit_module.py | 58 +++++++++++++++++++++----- app/scodoc/sco_evaluation_edit.py | 6 ++- app/scodoc/sco_moduleimpl_status.py | 4 +- app/static/css/scodoc.css | 10 +++++ app/templates/scodoc/help/modules.html | 20 +++++++++ sco_version.py | 2 +- 9 files changed, 102 insertions(+), 17 deletions(-) diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index f21e6acd..0caadb9b 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -40,6 +40,7 @@ from app import log from app import models from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError def df_load_evaluations_poids( @@ -60,7 +61,10 @@ def df_load_evaluations_poids( for eval_poids in EvaluationUEPoids.query.join( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): - df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids + try: + df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids + except KeyError as exc: + pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... if default_poids is not None: df.fillna(value=default_poids, inplace=True) return df, ues diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 4f06fb75..b4e5f4e2 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -103,7 +103,16 @@ class Evaluation(db.Model): Note: si les poids ne sont pas initialisés (poids par défaut), ils ne sont pas affichés. """ - return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids]) + # restreint aux UE du semestre dans lequel est cette évaluation + # au cas où le module ait changé de semestre et qu'il reste des poids + evaluation_semestre_idx = self.moduleimpl.module.semestre_id + return ", ".join( + [ + f"{p.ue.acronyme}: {p.poids}" + for p in self.ue_poids + if evaluation_semestre_idx == p.ue.semestre_idx + ] + ) class EvaluationUEPoids(db.Model): diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 450ea2cc..ca159f0f 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -224,7 +224,7 @@ class FormSemestre(db.Model): self.date_fin.year})""" def titre_num(self) -> str: - """Le titre est le semestre, ex ""DUT Informatique semestre 2"" """ + """Le titre et le semestre, ex ""DUT Informatique semestre 2"" """ if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID: return self.titre return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 25ec3d25..5ebf599d 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -37,6 +37,7 @@ from app import log from app import models from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns +from app.models import FormSemestre, ModuleImpl import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -473,16 +474,31 @@ def module_edit(module_id=None): formation_id = module["formation_id"] formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) - is_apc = parcours.APC_SAE - ues_matieres = ndb.SimpleDictFetch( - """SELECT ue.acronyme, mat.*, mat.id AS matiere_id - FROM notes_matieres mat, notes_ue ue - WHERE mat.ue_id = ue.id - AND ue.formation_id = %(formation_id)s - ORDER BY ue.numero, mat.numero - """, - {"formation_id": formation_id}, - ) + is_apc = parcours.APC_SAE # BUT + in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls + if in_use: + # matières du même semestre seulement + ues_matieres = ndb.SimpleDictFetch( + """SELECT ue.acronyme, mat.*, mat.id AS matiere_id + FROM notes_matieres mat, notes_ue ue + WHERE mat.ue_id = ue.id + AND ue.formation_id = %(formation_id)s + AND ue.semestre_idx = %(semestre_idx)s + ORDER BY ue.numero, mat.numero + """, + {"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx}, + ) + else: + # matières de la formation + ues_matieres = ndb.SimpleDictFetch( + """SELECT ue.acronyme, mat.*, mat.id AS matiere_id + FROM notes_matieres mat, notes_ue ue + WHERE mat.ue_id = ue.id + AND ue.formation_id = %(formation_id)s + ORDER BY ue.numero, mat.numero + """, + {"formation_id": formation_id}, + ) mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres] ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres] module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) @@ -501,12 +517,25 @@ def module_edit(module_id=None): ), """

Modification du module %(titre)s""" % module, """ (formation %(acronyme)s, version %(version)s)

""" % formation, - render_template("scodoc/help/modules.html", is_apc=is_apc), + render_template( + "scodoc/help/modules.html", + is_apc=is_apc, + formsemestres=FormSemestre.query.filter( + ModuleImpl.formsemestre_id == FormSemestre.id, + ModuleImpl.module_id == module_id, + ).all(), + ), ] if not unlocked: H.append( """
Formation verrouillée, seuls certains éléments peuvent être modifiés
""" ) + if in_use: + H.append( + """
Module déjà utilisé dans des semestres, + soyez prudents ! +
""" + ) descr = [ ( @@ -679,6 +708,13 @@ def module_edit(module_id=None): else: # l'UE peut changer tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") + old_ue_id = a_module.ue.id + new_ue_id = int(tf[2]["ue_id"]) + if (old_ue_id != new_ue_id) and in_use: + # pas changer de semestre un module utilisé ! + raise ScoValueError( + "Module utilisé: il ne peut pas être changé de semestre !" + ) # En APC, force le semestre égal à celui de l'UE if is_apc: selected_ue = UniteEns.query.get(tf[2]["ue_id"]) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index c324a89b..ab178c15 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -143,6 +143,7 @@ def evaluation_create_form( if vals.get("tf_submitted", False) and "visibulletinlist" not in vals: vals["visibulletinlist"] = [] # + ue_coef_dict = {} if is_apc: # BUT: poids vers les UE ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict() for ue in sem_ues: @@ -290,7 +291,10 @@ def evaluation_create_form( "title": f"Poids {ue.acronyme}", "size": 2, "type": "float", - "explanation": f"{ue.titre}", + "explanation": f""" + {ue_coef_dict.get(ue.id, 0.)} + {ue.titre} + """, "allow_null": False, }, ), diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 27de3a69..d44b5235 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -397,7 +397,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): eval_index = len(mod_evals) - 1 first_eval = True for eval in mod_evals: - evaluation = Evaluation.query.get(eval["evaluation_id"]) # TODO unifier + evaluation: Evaluation = Evaluation.query.get( + eval["evaluation_id"] + ) # TODO unifier etat = sco_evaluations.do_evaluation_etat( eval["evaluation_id"], partition_id=partition_id, diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 72a918a6..4ff17675 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1491,6 +1491,16 @@ table.moduleimpl_evaluations td.eval_poids { color:rgb(0, 0, 255); } +span.eval_coef_ue { + color:rgb(6, 73, 6); + font-style: normal; + font-size: 80%; + margin-right: 2em; +} +span.eval_coef_ue_titre { + +} + /* Formulaire edition des partitions */ form#editpart table { border: 1px solid gray; diff --git a/app/templates/scodoc/help/modules.html b/app/templates/scodoc/help/modules.html index d01a5d35..cd6e0767 100644 --- a/app/templates/scodoc/help/modules.html +++ b/app/templates/scodoc/help/modules.html @@ -24,4 +24,24 @@ la documentation.

{%endif%} + + {% if formsemestres %} +

+ Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention + aux conséquences des changements effectués ici: par exemple les coefficients vont modifier + les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits. + Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module. +

+

Semestres utilisant ce module:

+ + {%endif%} + \ No newline at end of file diff --git a/sco_version.py b/sco_version.py index 5ddad619..36fffe4e 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.28" +SCOVERSION = "9.1.29" SCONAME = "ScoDoc" From 687d5d65695994eec80c5f06a97e6876c145732f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 20 Jan 2022 13:14:04 +0100 Subject: [PATCH 10/70] =?UTF-8?q?BUT:=20Evite=20de=20planter=20si=20UE=20d?= =?UTF-8?q?e=20plusieurs=20semestres=20dans=20le=20m=C3=AAme=20formsemestr?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/notes_table.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index 2209aea7..4d586c2c 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -1359,7 +1359,11 @@ class NotesTable: t[0] = results.etud_moy_gen[etudid] for i, ue in enumerate(ues, start=1): if ue["type"] != UE_SPORT: - t[i] = results.etud_moy_ue[ue["id"]][etudid] + # temporaire pour 9.1.29 ! + if ue["id"] in results.etud_moy_ue: + t[i] = results.etud_moy_ue[ue["id"]][etudid] + else: + t[i] = "" # re-trie selon la nouvelle moyenne générale: self.T.sort(key=self._row_key) # Remplace aussi le rang: From a60811ce69844f19cc20c818e9f35982e8369178 Mon Sep 17 00:00:00 2001 From: "pascal.bouron" Date: Thu, 20 Jan 2022 22:41:39 +0100 Subject: [PATCH 11/70] =?UTF-8?q?Mise=20=C3=A0=20jour=20de=20'app/scodoc/s?= =?UTF-8?q?co=5Fundo=5Fnotes.py'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajout evaluation_id dans export de "lister toutes les saisies" --- app/scodoc/sco_undo_notes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_undo_notes.py b/app/scodoc/sco_undo_notes.py index 56684e39..5d169b8b 100644 --- a/app/scodoc/sco_undo_notes.py +++ b/app/scodoc/sco_undo_notes.py @@ -181,7 +181,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"): """ sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True) r = ndb.SimpleDictFetch( - """SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name + """SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi, notes_modules mod, identite i, "user" u WHERE mi.id = e.moduleimpl_id @@ -202,6 +202,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"): "value", "user_name", "titre", + "evaluation_id", "description", "jour", "comment", @@ -214,6 +215,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"): "value": "Note", "comment": "Remarque", "user_name": "Enseignant", + "evaluation_id": "evaluation_id", "titre": "Module", "description": "Evaluation", "jour": "Date éval.", From 90bff9ded6bdecbad4908490a42ac8565524f800 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 21 Jan 2022 00:46:45 +0100 Subject: [PATCH 12/70] =?UTF-8?q?Config;=20des=20codes=20Apog=C3=A9e.=20Cl?= =?UTF-8?q?oses=20#111.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/__init__.py | 4 +- app/models/preferences.py | 110 +------------------------------ app/scodoc/sco_apogee_csv.py | 49 +++----------- app/scodoc/sco_codes_parcours.py | 1 + app/scodoc/sco_groups.py | 8 ++- app/scodoc/sco_portal_apogee.py | 4 +- app/templates/configuration.html | 2 + app/views/scodoc.py | 53 +++++++++------ app/views/users.py | 2 +- scodoc.py | 8 --- 10 files changed, 59 insertions(+), 182 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index 4a3328b3..364943aa 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -12,7 +12,7 @@ GROUPNAME_STR_LEN = 64 from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence - +from app.models.config import ScoDocSiteConfig from app.models.departements import Departement from app.models.entreprises import ( @@ -63,7 +63,7 @@ from app.models.notes import ( NotesNotes, NotesNotesLog, ) -from app.models.preferences import ScoPreference, ScoDocSiteConfig +from app.models.preferences import ScoPreference from app.models.but_refcomp import ( ApcReferentielCompetences, diff --git a/app/models/preferences.py b/app/models/preferences.py index 59c82ec8..924f6e60 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -2,9 +2,8 @@ """Model : preferences """ -from app import db, log -from app.scodoc import bonus_sport -from app.scodoc.sco_exceptions import ScoValueError + +from app import db class ScoPreference(db.Model): @@ -19,108 +18,3 @@ class ScoPreference(db.Model): name = db.Column(db.String(128), nullable=False, index=True) value = db.Column(db.Text()) formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) - - -class ScoDocSiteConfig(db.Model): - """Config. d'un site - Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions - antérieures étaient dans scodoc_config.py - """ - - __tablename__ = "scodoc_site_config" - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(128), nullable=False, index=True) - value = db.Column(db.Text()) - - BONUS_SPORT = "bonus_sport_func_name" - NAMES = { - BONUS_SPORT: str, - "always_require_ine": bool, - "SCOLAR_FONT": str, - "SCOLAR_FONT_SIZE": str, - "SCOLAR_FONT_SIZE_FOOT": str, - "INSTITUTION_NAME": str, - "INSTITUTION_ADDRESS": str, - "INSTITUTION_CITY": str, - "DEFAULT_PDF_FOOTER_TEMPLATE": str, - } - - def __init__(self, name, value): - self.name = name - self.value = value - - def __repr__(self): - return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>" - - def get_dict(self) -> dict: - "Returns all data as a dict name = value" - return { - c.name: self.NAMES.get(c.name, lambda x: x)(c.value) - for c in ScoDocSiteConfig.query.all() - } - - @classmethod - def set_bonus_sport_func(cls, func_name): - """Record bonus_sport config. - If func_name not defined, raise NameError - """ - if func_name not in cls.get_bonus_sport_func_names(): - raise NameError("invalid function name for bonus_sport") - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c: - log("setting to " + func_name) - c.value = func_name - else: - c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name) - db.session.add(c) - db.session.commit() - - @classmethod - def get_bonus_sport_func_name(cls): - """Get configured bonus function name, or None if None.""" - f = cls.get_bonus_sport_func_from_name() - if f is None: - return "" - else: - return f.__name__ - - @classmethod - def get_bonus_sport_func(cls): - """Get configured bonus function, or None if None.""" - return cls.get_bonus_sport_func_from_name() - - @classmethod - def get_bonus_sport_func_from_name(cls, func_name=None): - """returns bonus func with specified name. - If name not specified, return the configured function. - None if no bonus function configured. - Raises ScoValueError if func_name not found in module bonus_sport. - """ - if func_name is None: - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c is None: - return None - func_name = c.value - if func_name == "": # pas de bonus défini - return None - try: - return getattr(bonus_sport, func_name) - except AttributeError: - raise ScoValueError( - f"""Fonction de calcul maison inexistante: {func_name}. - (contacter votre administrateur local).""" - ) - - @classmethod - def get_bonus_sport_func_names(cls): - """List available functions names - (starting with empty string to represent "no bonus function"). - """ - return [""] + sorted( - [ - getattr(bonus_sport, name).__name__ - for name in dir(bonus_sport) - if name.startswith("bonus_") - ] - ) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index f97cc895..833a7841 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -95,30 +95,21 @@ from flask import send_file # Pour la détection auto de l'encodage des fichiers Apogée: from chardet import detect as chardet_detect +from app.models.config import ScoDocSiteConfig import app.scodoc.sco_utils as scu -import app.scodoc.notesdb as ndb from app import log from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.gen_tables import GenTable from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_codes_parcours import code_semestre_validant from app.scodoc.sco_codes_parcours import ( - ADC, - ADJ, - ADM, - AJ, - ATB, - ATJ, - ATT, - CMP, DEF, + DEM, NAR, RAT, ) from app.scodoc import sco_cache -from app.scodoc import sco_codes_parcours from app.scodoc import sco_formsemestre -from app.scodoc import sco_formsemestre_status from app.scodoc import sco_parcours_dut from app.scodoc import sco_etud @@ -132,24 +123,6 @@ APO_SEP = "\t" APO_NEWLINE = "\r\n" -def code_scodoc_to_apo(code): - """Conversion code jury ScoDoc en code Apogée""" - return { - ATT: "AJAC", - ATB: "AJAC", - ATJ: "AJAC", - ADM: "ADM", - ADJ: "ADM", - ADC: "ADMC", - AJ: "AJ", - CMP: "COMP", - "DEM": "NAR", - DEF: "NAR", - NAR: "NAR", - RAT: "ATT", - }.get(code, "DEF") - - def _apo_fmt_note(note): "Formatte une note pour Apogée (séparateur décimal: ',')" if not note and isinstance(note, float): @@ -449,7 +422,7 @@ class ApoEtud(dict): N=_apo_fmt_note(ue_status["moy"]), B=20, J="", - R=code_scodoc_to_apo(code_decision_ue), + R=ScoDocSiteConfig.get_code_apo(code_decision_ue), M="", ) else: @@ -475,13 +448,9 @@ class ApoEtud(dict): def comp_elt_semestre(self, nt, decision, etudid): """Calcul résultat apo semestre""" # resultat du semestre - decision_apo = code_scodoc_to_apo(decision["code"]) + decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"]) note = nt.get_etud_moy_gen(etudid) - if ( - decision_apo == "DEF" - or decision["code"] == "DEM" - or decision["code"] == DEF - ): + if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF: note_str = "0,01" # note non nulle pour les démissionnaires else: note_str = _apo_fmt_note(note) @@ -520,21 +489,21 @@ class ApoEtud(dict): # ou jury intermediaire et etudiant non redoublant... return self.comp_elt_semestre(cur_nt, cur_decision, etudid) - decision_apo = code_scodoc_to_apo(cur_decision["code"]) + decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"]) autre_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"]) autre_decision = autre_nt.get_etud_decision_sem(etudid) if not autre_decision: # pas de decision dans l'autre => pas de résultat annuel return VOID_APO_RES - autre_decision_apo = code_scodoc_to_apo(autre_decision["code"]) + autre_decision_apo = ScoDocSiteConfig.get_code_apo(autre_decision["code"]) if ( autre_decision_apo == "DEF" - or autre_decision["code"] == "DEM" + or autre_decision["code"] == DEM or autre_decision["code"] == DEF ) or ( decision_apo == "DEF" - or cur_decision["code"] == "DEM" + or cur_decision["code"] == DEM or cur_decision["code"] == DEF ): note_str = "0,01" # note non nulle pour les démissionnaires diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 4ff29bb7..38d3e4fe 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -125,6 +125,7 @@ CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis) NAR = "NAR" RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission) +DEM = "DEM" # codes actions REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8248491a..87d50e3f 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -87,8 +87,10 @@ groupEditor = ndb.EditableTable( group_list = groupEditor.list -def get_group(group_id): +def get_group(group_id: int): """Returns group object, with partition""" + if not isinstance(group_id, int): + raise ValueError("invalid group_id (%s)" % group_id) r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* FROM group_descr gd, partition p @@ -687,6 +689,10 @@ def setGroups( group_id = fs[0].strip() if not group_id: continue + try: + group_id = int(group_id) + except ValueError as exc: + raise ValueError("invalid group_id: not an integer") group = get_group(group_id) # Anciens membres du groupe: old_members = get_group_members(group_id) diff --git a/app/scodoc/sco_portal_apogee.py b/app/scodoc/sco_portal_apogee.py index f78e9003..836be2ed 100644 --- a/app/scodoc/sco_portal_apogee.py +++ b/app/scodoc/sco_portal_apogee.py @@ -169,7 +169,9 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2): if doc: break if not doc: - raise ScoValueError("pas de réponse du portail ! (timeout=%s)" % portal_timeout) + raise ScoValueError( + f"pas de réponse du portail !
(timeout={portal_timeout}, requête: {req})" + ) etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req)) # Filtre sur annee inscription Apogee: diff --git a/app/templates/configuration.html b/app/templates/configuration.html index b874d48d..f9d060ad 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -92,6 +92,8 @@
Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):
{{ render_field(form.bonus_sport_func_name, onChange="submit_form()")}} +

Exports Apogée

+

configuration des codes de décision

Bibliothèque de logos

{% for dept_entry in form.depts.entries %} {% set dept_form = dept_entry.form %} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index a7aedaa8..02bc1fb1 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -33,49 +33,38 @@ Emmanuel Viennet, 2021 import datetime import io -import wtforms.validators - -from app.auth.models import User -import os - import flask from flask import abort, flash, url_for, redirect, render_template, send_file from flask import request -from flask.app import Flask import flask_login from flask_login.utils import login_required, current_user -from flask_wtf import FlaskForm -from flask_wtf.file import FileField, FileAllowed -from werkzeug.exceptions import BadRequest, NotFound -from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList -from wtforms.fields import IntegerField -from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField -from wtforms.validators import ValidationError, DataRequired, Email, EqualTo +from PIL import Image as PILImage + +from werkzeug.exceptions import BadRequest, NotFound + -import app from app import db +from app.auth.models import User from app.forms.main import config_forms from app.forms.main.create_dept import CreateDeptForm +from app.forms.main.config_apo import CodesDecisionsForm +from app import models from app.models import Departement, Identite from app.models import departements from app.models import FormSemestre, FormSemestreInscription -import sco_version -from app.scodoc import sco_logos +from app.models import ScoDocSiteConfig +from app.scodoc import sco_codes_parcours, sco_logos from app.scodoc import sco_find_etud from app.scodoc import sco_utils as scu from app.decorators import ( admin_required, scodoc7func, scodoc, - permission_required_compat_scodoc7, - permission_required, ) from app.scodoc.sco_exceptions import AccessDenied -from app.scodoc.sco_logos import find_logo from app.scodoc.sco_permissions import Permission from app.views import scodoc_bp as bp - -from PIL import Image as PILImage +import sco_version @bp.route("/") @@ -132,6 +121,28 @@ def toggle_dept_vis(dept_id): return redirect(url_for("scodoc.index")) +@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"]) +@admin_required +def config_codes_decisions(): + """Form config codes decisions""" + form = CodesDecisionsForm() + if request.method == "POST" and form.cancel.data: # cancel button + return redirect(url_for("scodoc.index")) + if form.validate_on_submit(): + for code in models.config.CODES_SCODOC_TO_APO: + ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data) + flash(f"Codes décisions enregistrés.") + return redirect(url_for("scodoc.index")) + elif request.method == "GET": + for code in models.config.CODES_SCODOC_TO_APO: + getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code) + return render_template( + "config_codes_decisions.html", + form=form, + title="Configuration des codes de décisions", + ) + + @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @login_required def table_etud_in_accessible_depts(): diff --git a/app/views/users.py b/app/views/users.py index cc9b9db0..fe65348c 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -81,7 +81,7 @@ _l = _ class ChangePasswordForm(FlaskForm): user_name = HiddenField() old_password = PasswordField(_l("Identifiez-vous")) - new_password = PasswordField(_l("Nouveau mot de passe")) + new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur")) bis_password = PasswordField( _l("Répéter"), validators=[ diff --git a/scodoc.py b/scodoc.py index 976443a3..f18f0892 100755 --- a/scodoc.py +++ b/scodoc.py @@ -300,14 +300,6 @@ def delete_dept(dept): # delete-dept from app.scodoc import notesdb as ndb from app.scodoc import sco_dept - if False: - click.confirm( - f"""Attention: Cela va effacer toutes les données du département {dept} - (étudiants, notes, formations, etc) - Voulez-vous vraiment continuer ? - """, - abort=True, - ) db.reflect() ndb.open_db_connection() d = models.Departement.query.filter_by(acronym=dept).first() From 51933d057b0c88d5bdcfd2c5aff3a94c3accafcb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 21 Jan 2022 10:27:47 +0100 Subject: [PATCH 13/70] Morceaux manquants. --- app/forms/main/config_apo.py | 76 +++++++++ app/models/config.py | 178 ++++++++++++++++++++++ app/templates/config_codes_decisions.html | 23 +++ sco_version.py | 2 +- 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 app/forms/main/config_apo.py create mode 100644 app/models/config.py create mode 100644 app/templates/config_codes_decisions.html diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py new file mode 100644 index 00000000..43ed6282 --- /dev/null +++ b/app/forms/main/config_apo.py @@ -0,0 +1,76 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +""" +Formulaires configuration Exports Apogée (codes) +""" +import re + +from flask import flash, url_for, redirect, render_template +from flask_wtf import FlaskForm +from wtforms import SubmitField, validators +from wtforms.fields.simple import StringField + +from app import models +from app.models import ScoDocSiteConfig +from app.models import SHORT_STR_LEN + +from app.scodoc import sco_utils as scu + + +def _build_code_field(code): + return StringField( + label=code, + validators=[ + validators.regexp( + r"^[A-Z0-9_]*$", + message="Ne doit comporter que majuscules et des chiffres", + ), + validators.Length( + max=SHORT_STR_LEN, + message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères", + ), + validators.DataRequired("code requis"), + ], + ) + + +class CodesDecisionsForm(FlaskForm): + ADC = _build_code_field("ADC") + ADJ = _build_code_field("ADJ") + ADM = _build_code_field("ADM") + AJ = _build_code_field("AJ") + ATB = _build_code_field("ATB") + ATJ = _build_code_field("ATJ") + ATT = _build_code_field("ATT") + CMP = _build_code_field("CMP") + DEF = _build_code_field("DEF") + DEM = _build_code_field("DEF") + NAR = _build_code_field("NAR") + RAT = _build_code_field("RAT") + submit = SubmitField("Valider") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/config.py b/app/models/config.py new file mode 100644 index 00000000..af04ee51 --- /dev/null +++ b/app/models/config.py @@ -0,0 +1,178 @@ +# -*- coding: UTF-8 -* + +"""Model : site config WORK IN PROGRESS #WIP +""" + +from app import db, log +from app.scodoc import bonus_sport +from app.scodoc.sco_exceptions import ScoValueError +import functools + +from app.scodoc.sco_codes_parcours import ( + ADC, + ADJ, + ADM, + AJ, + ATB, + ATJ, + ATT, + CMP, + DEF, + DEM, + NAR, + RAT, +) + +CODES_SCODOC_TO_APO = { + ADC: "ADMC", + ADJ: "ADM", + ADM: "ADM", + AJ: "AJ", + ATB: "AJAC", + ATJ: "AJAC", + ATT: "AJAC", + CMP: "COMP", + DEF: "NAR", + DEM: "NAR", + NAR: "NAR", + RAT: "ATT", +} + + +def code_scodoc_to_apo_default(code): + """Conversion code jury ScoDoc en code Apogée + (codes par défaut, c'est configurable via ScoDocSiteConfig.get_code_apo) + """ + return CODES_SCODOC_TO_APO.get(code, "DEF") + + +class ScoDocSiteConfig(db.Model): + """Config. d'un site + Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions + antérieures étaient dans scodoc_config.py + """ + + __tablename__ = "scodoc_site_config" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(128), nullable=False, index=True) + value = db.Column(db.Text()) + + BONUS_SPORT = "bonus_sport_func_name" + NAMES = { + BONUS_SPORT: str, + "always_require_ine": bool, + "SCOLAR_FONT": str, + "SCOLAR_FONT_SIZE": str, + "SCOLAR_FONT_SIZE_FOOT": str, + "INSTITUTION_NAME": str, + "INSTITUTION_ADDRESS": str, + "INSTITUTION_CITY": str, + "DEFAULT_PDF_FOOTER_TEMPLATE": str, + } + + def __init__(self, name, value): + self.name = name + self.value = value + + def __repr__(self): + return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>" + + @classmethod + def get_dict(cls) -> dict: + "Returns all data as a dict name = value" + return { + c.name: cls.NAMES.get(c.name, lambda x: x)(c.value) + for c in ScoDocSiteConfig.query.all() + } + + @classmethod + def set_bonus_sport_func(cls, func_name): + """Record bonus_sport config. + If func_name not defined, raise NameError + """ + if func_name not in cls.get_bonus_sport_func_names(): + raise NameError("invalid function name for bonus_sport") + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c: + log("setting to " + func_name) + c.value = func_name + else: + c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name) + db.session.add(c) + db.session.commit() + + @classmethod + def get_bonus_sport_func_name(cls): + """Get configured bonus function name, or None if None.""" + f = cls.get_bonus_sport_func_from_name() + if f is None: + return "" + else: + return f.__name__ + + @classmethod + def get_bonus_sport_func(cls): + """Get configured bonus function, or None if None.""" + return cls.get_bonus_sport_func_from_name() + + @classmethod + def get_bonus_sport_func_from_name(cls, func_name=None): + """returns bonus func with specified name. + If name not specified, return the configured function. + None if no bonus function configured. + Raises ScoValueError if func_name not found in module bonus_sport. + """ + if func_name is None: + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c is None: + return None + func_name = c.value + if func_name == "": # pas de bonus défini + return None + try: + return getattr(bonus_sport, func_name) + except AttributeError: + raise ScoValueError( + f"""Fonction de calcul maison inexistante: {func_name}. + (contacter votre administrateur local).""" + ) + + @classmethod + def get_bonus_sport_func_names(cls): + """List available functions names + (starting with empty string to represent "no bonus function"). + """ + return [""] + sorted( + [ + getattr(bonus_sport, name).__name__ + for name in dir(bonus_sport) + if name.startswith("bonus_") + ] + ) + + @classmethod + def get_code_apo(cls, code: str) -> str: + """La représentation d'un code pour les exports Apogée. + Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL + Les codes par défaut sont donnés dans sco_apogee_csv. + + """ + cfg = ScoDocSiteConfig.query.filter_by(name=code).first() + if not cfg: + code_apo = code_scodoc_to_apo_default(code) + else: + code_apo = cfg.value + return code_apo + + @classmethod + def set_code_apo(cls, code: str, code_apo: str): + """Enregistre nouvelle représentation du code""" + if code_apo != cls.get_code_apo(code): + cfg = ScoDocSiteConfig.query.filter_by(name=code).first() + if cfg is None: + cfg = ScoDocSiteConfig(code, code_apo) + else: + cfg.value = code_apo + db.session.add(cfg) + db.session.commit() diff --git a/app/templates/config_codes_decisions.html b/app/templates/config_codes_decisions.html new file mode 100644 index 00000000..0c2f32b2 --- /dev/null +++ b/app/templates/config_codes_decisions.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

Configuration des codes de décision exportés vers Apogée

+ + +
+

Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury +et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées +dans les exports Apogée. +

+

Ne les modifier que si vous savez ce que vous faites ! +

+
+
+
+ {{ wtf.quick_form(form) }} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/sco_version.py b/sco_version.py index 36fffe4e..39b1b340 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.29" +SCOVERSION = "9.1.30" SCONAME = "ScoDoc" From d64ecdffcba503e33afa505580d6e1af40f9c45e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 21 Jan 2022 18:09:15 +0100 Subject: [PATCH 14/70] =?UTF-8?q?Fix:=20acc=C3=A8s=20aux=20groupes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_groups.py | 2 -- sco_version.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 87d50e3f..b06a8e46 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -89,8 +89,6 @@ group_list = groupEditor.list def get_group(group_id: int): """Returns group object, with partition""" - if not isinstance(group_id, int): - raise ValueError("invalid group_id (%s)" % group_id) r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* FROM group_descr gd, partition p diff --git a/sco_version.py b/sco_version.py index 39b1b340..3c171d1f 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.30" +SCOVERSION = "9.1.31" SCONAME = "ScoDoc" From 4993dc4df3bff05c2038b270a6c2e4f2a0e29d98 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 21 Jan 2022 18:46:00 +0100 Subject: [PATCH 15/70] setGroups: ignore groupes invalides --- app/scodoc/sco_groups.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index b06a8e46..e14ae526 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -690,7 +690,8 @@ def setGroups( try: group_id = int(group_id) except ValueError as exc: - raise ValueError("invalid group_id: not an integer") + log("setGroups: ignoring invalid group_id={group_id}") + continue group = get_group(group_id) # Anciens membres du groupe: old_members = get_group_members(group_id) From 7c89b9a8d327a290e50bb8b674f4ffc1f70472ca Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 21 Jan 2022 22:03:16 +0100 Subject: [PATCH 16/70] Message d'erreur si upload notes xls avec etudid invalide --- app/scodoc/sco_saisie_notes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index d0a5407d..9ccbd9c8 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -153,7 +153,10 @@ def _check_notes(notes, evaluation, mod): for (etudid, note) in notes: note = str(note).strip().upper() - etudid = int(etudid) # + try: + etudid = int(etudid) # + except ValueError as exc: + raise ScoValueError(f"Code étudiant ({etudid}) invalide") if note[:3] == "DEM": continue # skip ! if note: From 66a1ba46c33342ed73b05c33924aa2819818c912 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Fri, 21 Jan 2022 23:25:02 +0100 Subject: [PATCH 17/70] convert to RGB (from ARGB) when saving as JPEG --- app/views/scodoc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 02bc1fb1..394dfe83 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -266,14 +266,16 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True): suffix = logo.suffix if small: with PILImage.open(logo.filepath) as im: - im.thumbnail(SMALL_SIZE) - stream = io.BytesIO() # on garde le même format (on pourrait plus simplement générer systématiquement du JPEG) fmt = { # adapt suffix to be compliant with PIL save format "PNG": "PNG", "JPG": "JPEG", "JPEG": "JPEG", }[suffix.upper()] + if fmt == "JPEG": + im = im.convert("RGB") + im.thumbnail(SMALL_SIZE) + stream = io.BytesIO() im.save(stream, fmt) stream.seek(0) return send_file(stream, mimetype=f"image/{fmt}") From 4df39361fbc9b7fae6d12adbe8fdafe12051bf98 Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Sat, 22 Jan 2022 00:14:39 +0100 Subject: [PATCH 18/70] now support several departement for a nip when load formsemestre_bulletinetud --- app/views/notes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/views/notes.py b/app/views/notes.py index 35c9ca83..8b050966 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -290,9 +290,12 @@ def formsemestre_bulletinetud( if etudid: etud = models.Identite.query.get_or_404(etudid) elif code_nip: - etud = models.Identite.query.filter_by( - code_nip=str(code_nip) - ).first_or_404() + dept = formsemestre.dept_id + etud = ( + models.Identite.query.filter_by(code_nip=str(code_nip)) + .filter_by(dept_id=dept) + .first_or_404() + ) elif code_ine: etud = models.Identite.query.filter_by( code_ine=str(code_ine) From 53ae043ffa60f044d7d86eeedbefb53b114efa58 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 22 Jan 2022 11:34:57 +0100 Subject: [PATCH 19/70] Fix: sanitize_old_formation --- app/models/formations.py | 4 ++-- sco_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/formations.py b/app/models/formations.py index e2273c3b..b69d566a 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -1,6 +1,7 @@ """ScoDoc 9 models : Formations """ +import app from app import db from app.comp import df_cache from app.models import SHORT_STR_LEN @@ -141,8 +142,7 @@ class Formation(db.Model): db.session.add(ue) db.session.commit() - if change: - self.invalidate_module_coefs() + app.clear_scodoc_cache() class Matiere(db.Model): diff --git a/sco_version.py b/sco_version.py index 3c171d1f..55a2c785 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.31" +SCOVERSION = "9.1.32" SCONAME = "ScoDoc" From 3e20bd8198917edc67d7d24a0660df941b129403 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 22 Jan 2022 12:15:03 +0100 Subject: [PATCH 20/70] Explication des codes jury --- app/forms/main/config_apo.py | 2 ++ app/scodoc/sco_codes_parcours.py | 14 +++++++++----- app/scodoc/sco_formsemestre_validation.py | 2 +- app/scodoc/sco_pvjury.py | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index 43ed6282..a655f450 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -39,12 +39,14 @@ from app import models from app.models import ScoDocSiteConfig from app.models import SHORT_STR_LEN +from app.scodoc import sco_codes_parcours from app.scodoc import sco_utils as scu def _build_code_field(code): return StringField( label=code, + description=sco_codes_parcours.CODES_EXPL[code], validators=[ validators.regexp( r"^[A-Z0-9_]*$", diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 38d3e4fe..6bcb8cc3 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -141,22 +141,26 @@ BUG = "BUG" ALL = "ALL" +# Explication des codes (de demestre ou d'UE) CODES_EXPL = { - ADM: "Validé", ADC: "Validé par compensation", ADJ: "Validé par le Jury", - ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)", + ADM: "Validé", + AJ: "Ajourné", ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)", ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)", - AJ: "Ajourné", - NAR: "Echec, non autorisé à redoubler", - RAT: "En attente d'un rattrapage", + ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)", + CMP: "Code UE acquise car semestre acquis", DEF: "Défaillant", + NAR: "Échec, non autorisé à redoubler", + RAT: "En attente d'un rattrapage", } # Nota: ces explications sont personnalisables via le fichier # de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py # variable: CONFIG.CODES_EXP +# Les codes de semestres: +CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 86525c5c..3f4eb79f 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -738,7 +738,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None ) # Choix code semestre: - codes = list(sco_codes_parcours.CODES_EXPL.keys()) + codes = list(sco_codes_parcours.CODES_JURY_SEM) codes.sort() # fortuitement, cet ordre convient bien ! H.append( diff --git a/app/scodoc/sco_pvjury.py b/app/scodoc/sco_pvjury.py index d193b373..e2f28c69 100644 --- a/app/scodoc/sco_pvjury.py +++ b/app/scodoc/sco_pvjury.py @@ -567,7 +567,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True): if "prev_decision" in row and row["prev_decision"]: counts[row["prev_decision"]] += 0 # Légende des codes - codes = list(counts.keys()) # sco_codes_parcours.CODES_EXPL.keys() + codes = list(counts.keys()) codes.sort() H.append("

Explication des codes

") lines = [] From 264ef7e1ff8fcc6ddbf4caf977c4a255bc6015e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 22 Jan 2022 17:37:04 +0100 Subject: [PATCH 21/70] formsemestre_bulletinetud avec arg INE: filtre sur dept --- app/views/notes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/views/notes.py b/app/views/notes.py index 8b050966..4be64afb 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -290,16 +290,17 @@ def formsemestre_bulletinetud( if etudid: etud = models.Identite.query.get_or_404(etudid) elif code_nip: - dept = formsemestre.dept_id etud = ( models.Identite.query.filter_by(code_nip=str(code_nip)) - .filter_by(dept_id=dept) + .filter_by(dept_id=formsemestre.dept_id) .first_or_404() ) elif code_ine: - etud = models.Identite.query.filter_by( - code_ine=str(code_ine) - ).first_or_404() + etud = ( + models.Identite.query.filter_by(code_ine=str(code_ine)) + .filter_by(dept_id=formsemestre.dept_id) + .first_or_404() + ) else: raise ScoValueError( "Paramètre manquant: spécifier code_nip ou etudid ou code_ine" From 31d48a56ff432175994ffabf1a15dfbc1ab49b3b Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Tue, 25 Jan 2022 08:44:20 +0100 Subject: [PATCH 22/70] fix_user ; always send from no-reply ; reset passwd_temp --- app/auth/models.py | 1 + app/views/users.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/auth/models.py b/app/auth/models.py index 8f187b7e..32768df0 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -112,6 +112,7 @@ class User(UserMixin, db.Model): self.password_hash = generate_password_hash(password) else: self.password_hash = None + self.passwd_temp = False def check_password(self, password): """Check given password vs current one. diff --git a/app/views/users.py b/app/views/users.py index fe65348c..59175344 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -154,7 +154,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1): if user_name is not None: # scodoc7func converti en int ! user_name = str(user_name) auth_dept = current_user.dept - from_mail = current_user.email + from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email initvalues = {} edit = int(edit) all_roles = int(all_roles) @@ -577,8 +577,8 @@ def create_user_form(user_name=None, edit=0, all_roles=1): # A: envoi de welcome + procedure de reset # B: envoi de welcome seulement (mot de passe saisie dans le formulaire) # C: Aucun envoi (mot de passe saisi dans le formulaire) - if vals["welcome"] == "1": - if vals["reset_password:list"] == "1": + if vals["welcome"] != "1": + if vals["reset_password"] != "1": mode = Mode.WELCOME_AND_CHANGE_PASSWORD else: mode = Mode.WELCOME_ONLY From 8385941cf6f4a1f43826468dec2fcaa8f2a3380f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 25 Jan 2022 10:45:13 +0100 Subject: [PATCH 23/70] =?UTF-8?q?WIP:=20calcul=20unifi=C3=A9,=20bonus=20sp?= =?UTF-8?q?ort=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 22 +- app/but/bulletin_but_xml_compat.py | 8 +- app/comp/aux.py | 14 +- app/comp/bonus_spo.py | 322 ++++++++++++++++++ app/comp/inscr_mod.py | 8 +- app/comp/moy_mod.py | 22 +- app/comp/moy_sem.py | 2 +- app/comp/moy_ue.py | 63 ++-- app/comp/res_but.py | 29 +- app/comp/res_classic.py | 44 ++- app/comp/res_common.py | 66 ++-- app/forms/main/config_forms.py | 6 +- app/models/formsemestre.py | 25 ++ app/models/moduleimpls.py | 15 +- app/models/modules.py | 5 +- app/models/preferences.py | 90 +++-- app/models/ues.py | 2 + app/scodoc/TrivialFormulator.py | 14 +- app/scodoc/bonus_sport.py | 3 +- app/scodoc/htmlutils.py | 13 +- app/scodoc/notes_table.py | 3 +- app/scodoc/sco_bulletins.py | 15 +- app/scodoc/sco_bulletins_json.py | 4 +- app/scodoc/sco_bulletins_xml.py | 9 +- app/scodoc/sco_cache.py | 3 +- app/scodoc/sco_config_actions.py | 2 +- app/scodoc/sco_edit_ue.py | 9 + app/scodoc/sco_formsemestre_status.py | 17 +- app/scodoc/sco_liste_notes.py | 28 +- app/scodoc/sco_moduleimpl_status.py | 4 +- app/static/css/scodoc.css | 14 + app/templates/pn/form_ues.html | 2 + .../versions/c95d5a3bf0de_couleur_ue.py | 28 ++ scodoc.py | 2 +- 34 files changed, 757 insertions(+), 156 deletions(-) create mode 100644 app/comp/bonus_spo.py create mode 100644 migrations/versions/c95d5a3bf0de_couleur_ue.py diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 2c6288bb..da7677d4 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -13,6 +13,7 @@ from flask import url_for, g from app.scodoc import sco_utils as scu from app.scodoc import sco_bulletins_json from app.scodoc import sco_preferences +from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import fmt_note from app.comp.res_but import ResultatsSemestreBUT @@ -49,23 +50,30 @@ class BulletinBUT(ResultatsSemestreBUT): d = { "id": ue.id, "numero": ue.numero, + "type": ue.type, "ECTS": { "acquis": 0, # XXX TODO voir jury "total": ue.ects, }, + "color": ue.color, "competence": None, # XXX TODO lien avec référentiel - "moyenne": { - "value": fmt_note(self.etud_moy_ue[ue.id][etud.id]), - "min": fmt_note(self.etud_moy_ue[ue.id].min()), - "max": fmt_note(self.etud_moy_ue[ue.id].max()), - "moy": fmt_note(self.etud_moy_ue[ue.id].mean()), - }, - "bonus": None, # XXX TODO + "moyenne": None, + # Le bonus sport appliqué sur cette UE + "bonus": self.bonus_ues[ue.id][etud.id] + if self.bonus_ues is not None and ue.id in self.bonus_ues + else 0.0, "malus": None, # XXX TODO voir ce qui est ici "capitalise": None, # "AAAA-MM-JJ" TODO "ressources": self.etud_ue_mod_results(etud, ue, self.ressources), "saes": self.etud_ue_mod_results(etud, ue, self.saes), } + if ue.type != UE_SPORT: + d["moyenne"] = { + "value": fmt_note(self.etud_moy_ue[ue.id][etud.id]), + "min": fmt_note(self.etud_moy_ue[ue.id].min()), + "max": fmt_note(self.etud_moy_ue[ue.id].max()), + "moy": fmt_note(self.etud_moy_ue[ue.id].mean()), + } return d def etud_mods_results(self, etud, modimpls) -> dict: diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index f318f236..69b8f568 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -134,8 +134,12 @@ def bulletin_but_xml_compat( moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen. ) ) - rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative - bonus = 0 # XXX TODO valeur du bonus sport + rang = 0 # XXX TODO rang de l'étudiant selon la moy gen indicative + # valeur du bonus sport + if results.bonus is not None: + bonus = results.bonus[etud.id] + else: + bonus = 0 doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits))) # XXX TODO: ajouter "rang_group" : rangs dans les partitions doc.append(Element("note_max", value="20")) # notes toujours sur 20 diff --git a/app/comp/aux.py b/app/comp/aux.py index 6a758a64..07517f36 100644 --- a/app/comp/aux.py +++ b/app/comp/aux.py @@ -19,12 +19,16 @@ class StatsMoyenne: def __init__(self, vals): """Calcul les statistiques. Les valeurs NAN ou non numériques sont toujours enlevées. + Si vals is None, renvoie des zéros (utilisé pour UE bonus) """ - self.moy = np.nanmean(vals) - self.min = np.nanmin(vals) - self.max = np.nanmax(vals) - self.size = len(vals) - self.nb_vals = self.size - np.count_nonzero(np.isnan(vals)) + if vals is None: + self.moy = self.min = self.max = self.size = self.nb_vals = 0 + else: + self.moy = np.nanmean(vals) + self.min = np.nanmin(vals) + self.max = np.nanmax(vals) + self.size = len(vals) + self.nb_vals = self.size - np.count_nonzero(np.isnan(vals)) def to_dict(self): "Tous les attributs dans un dict" diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py new file mode 100644 index 00000000..bba0cd47 --- /dev/null +++ b/app/comp/bonus_spo.py @@ -0,0 +1,322 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Classes spécifiques de calcul du bonus sport, culture ou autres activités + +Les classes de Bonus fournissent deux méthodes: + - get_bonus_ues() + - get_bonus_moy_gen() + + +""" +import numpy as np +import pandas as pd + +from app import db +from app import models +from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef +from app.comp import moy_mod +from app.models.formsemestre import FormSemestre +from app.scodoc import bonus_sport +from app.scodoc.sco_codes_parcours import UE_SPORT +from app.scodoc.sco_utils import ModuleType + + +def get_bonus_sport_class_from_name(dept_id): + """La classe de bonus sport pour le département indiqué. + Note: en ScoDoc 9, le bonus sport est défini gloabelement et + ne dépend donc pas du département. + Résultat: une sous-classe de BonusSport + """ + raise NotImplementedError() + + +class BonusSport: + """Calcul du bonus sport. + + Arguments: + - sem_modimpl_moys : + notes moyennes aux modules (tous les étuds x tous les modimpls) + floats avec des NaN. + En classique: sem_matrix, ndarray (etuds x modimpls) + En APC: sem_cube, ndarray (etuds x modimpls x UEs) + - ues: les ues du semestre (incluant le bonus sport) + - modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl) + - modimpl_coefs: les coefs des modules + En classique: 1d ndarray de float (modimpl) + En APC: 2d ndarray de float, (modimpl x UE) <= attention à transposer + """ + + # Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen: + apc_apply_bonus_mg_to_ues = True + # Attributs virtuels: + seuil_moy_gen = None + proportion_point = None + bonus_moy_gen_limit = None + + name = "virtual" + + def __init__( + self, + formsemestre: FormSemestre, + sem_modimpl_moys: np.array, + ues: list, + modimpl_inscr_df: pd.DataFrame, + modimpl_coefs: np.array, + ): + self.formsemestre = formsemestre + self.ues = ues + self.etuds_idx = modimpl_inscr_df.index # les étudiants inscrits au semestre + self.bonus_ues: pd.DataFrame = None # virtual + self.bonus_moy_gen: pd.Series = None # virtual + # Restreint aux modules standards des UE de type "sport": + modimpl_mask = np.array( + [ + (m.module.module_type == ModuleType.STANDARD) + and (m.module.ue.type == UE_SPORT) + for m in formsemestre.modimpls_sorted + ] + ) + self.modimpls_spo = [ + modimpl + for i, modimpl in enumerate(formsemestre.modimpls_sorted) + if modimpl_mask[i] + ] + "liste des modimpls sport" + + # Les moyennes des modules "sport": (une par UE en APC) + sem_modimpl_moys_spo = sem_modimpl_moys[:, modimpl_mask] + # Les inscriptions aux modules sport: + modimpl_inscr_spo = modimpl_inscr_df.values[:, modimpl_mask] + # Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue) + modimpl_coefs_spo = modimpl_coefs[modimpl_mask] + # sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport) + # ou (nb_etuds, nb_mod_sport, nb_ues) + nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2] + nb_ues = len(ues) + # Enlève les NaN du numérateur: + sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0) + + # Annule les coefs des modules où l'étudiant n'est pas inscrit: + if formsemestre.formation.is_apc(): + # BUT + nb_ues_no_bonus = sem_modimpl_moys.shape[2] + # Duplique les inscriptions sur les UEs non bonus: + modimpl_inscr_spo_stacked = np.stack( + [modimpl_inscr_spo] * nb_ues_no_bonus, axis=2 + ) + # Ne prend pas en compte les notes des étudiants non inscrits au module: + # Annule les notes: + sem_modimpl_moys_inscrits = np.where( + modimpl_inscr_spo_stacked, sem_modimpl_moys_no_nan, 0.0 + ) + # Annule les coefs des modules où l'étudiant n'est pas inscrit: + modimpl_coefs_etuds = np.where( + modimpl_inscr_spo_stacked, + np.stack([modimpl_coefs_spo.T] * nb_etuds), + 0.0, + ) + else: + # Formations classiques + # Ne prend pas en compte les notes des étudiants non inscrits au module: + # Annule les notes: + sem_modimpl_moys_inscrits = np.where( + modimpl_inscr_spo, sem_modimpl_moys_no_nan, 0.0 + ) + modimpl_coefs_spo = modimpl_coefs_spo.T + modimpl_coefs_etuds = np.where( + modimpl_inscr_spo, np.stack([modimpl_coefs_spo] * nb_etuds), 0.0 + ) + # Annule les coefs des modules NaN (nb_etuds x nb_mod_sport) + modimpl_coefs_etuds_no_nan = np.where( + np.isnan(sem_modimpl_moys_spo), 0.0, modimpl_coefs_etuds + ) + # + self.compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) + + def compute_bonus( + self, + sem_modimpl_moys_inscrits: np.ndarray, + modimpl_coefs_etuds_no_nan: np.ndarray, + ): + """Calcul des bonus: méthode virtuelle à écraser. + Arguments: + - sem_modimpl_moys_inscrits: + ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue) + les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans. + - modimpl_coefs_etuds_no_nan: + les coefficients: float ndarray + + Résultat: None + """ + raise NotImplementedError("méthode virtuelle") + + def get_bonus_ues(self) -> pd.Series: + """Les bonus à appliquer aux UE + Résultat: DataFrame de float, index etudid, columns: ue.id + """ + if ( + self.formsemestre.formation.is_apc() + and self.apc_apply_bonus_mg_to_ues + and self.bonus_ues is None + ): + # reporte uniformément le bonus moyenne générale sur les UEs + # (assure la compatibilité de la plupart des anciens bonus avec le BUT) + # ues = self.formsemestre.query_ues(with_sport=False) + ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)] + bonus_moy_gen = self.get_bonus_moy_gen() + bonus_ues = np.stack([bonus_moy_gen.values] * len(ues_idx), axis=1) + return pd.DataFrame(bonus_ues, index=self.etuds_idx, columns=ues_idx) + + return self.bonus_ues + + def get_bonus_moy_gen(self): + """Le bonus à appliquer à la moyenne générale. + Résultat: Series de float, index etudid + """ + return self.bonus_moy_gen + + +class BonusSportSimples(BonusSport): + """Les bonus sport simples calcule un bonus à partir des notes moyennes de modules + de l'UE sport, et ce bonus est soit appliqué sur la moyenne générale (formations classiques), + soit réparti sur les UE (formations APC). + + Le bonus est par défaut calculé comme: + Les points au-dessus du seuil (par défaut) 10 sur 20 obtenus dans chacun des + modules optionnels sont cumulés et une fraction de ces points cumulés s'ajoute + à la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés + proportion_point = 0.05 # multiplie les points au dessus du seuil + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + bonus_moy_gen_arr = np.sum( + np.where( + sem_modimpl_moys_inscrits > self.seuil_moy_gen, + (sem_modimpl_moys_inscrits - self.seuil_moy_gen) + * self.proportion_point, + 0.0, + ), + axis=1, + ) + # en APC, applati la moyenne gen. XXX pourrait être fait en amont + if len(bonus_moy_gen_arr.shape) > 1: + bonus_moy_gen_arr = bonus_moy_gen_arr.sum(axis=1) + # Bonus moyenne générale, et 0 sur les UE + self.bonus_moy_gen = pd.Series( + bonus_moy_gen_arr, index=self.etuds_idx, dtype=float + ) + if self.bonus_moy_gen_limit is not None: + # Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points + self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit) + + # Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs. + + +# bonus_ue = np.stack([modimpl_coefs_spo.T] * nb_ues) + + +class BonusIUTV(BonusSportSimples): + """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Paris 13 (sports, musique, deuxième langue, + culture, etc) non rattachés à une unité d'enseignement. Les points + au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + name = "bonus_iutv" + pass # oui, c'ets le bonus par défaut + + +class BonusDirect(BonusSportSimples): + """Bonus direct: les points sont directement ajoutés à la moyenne générale. + Les coefficients sont ignorés: tous les points de bonus sont sommés. + (rappel: la note est ramenée sur 20 avant application). + """ + + name = "bonus_direct" + seuil_moy_gen = 0.0 # seuls le spoints au dessus du seuil sont comptés + proportion_point = 1.0 + + +class BonusIUTStDenis(BonusIUTV): + """Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.""" + + name = "bonus_iut_stdenis" + bonus_moy_gen_limit = 0.5 + + +class BonusColmar(BonusSportSimples): + """Calcul bonus modules optionels (sport, culture), règle IUT Colmar. + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non + rattachés à une unité d'enseignement. Les points au-dessus de 10 + sur 20 obtenus dans chacune des matières optionnelles sont cumulés + dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + # note: cela revient à dire que l'on ajoute 5% des points au dessus de 10, + # et qu'on limite à 5% de 10, soit 0.5 points + # ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis) + name = "bonus_colmar" + bonus_moy_gen_limit = 0.5 + + +class BonusVilleAvray: + """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement. + Si la note est >= 10 et < 12, bonus de 0.1 point + Si la note est >= 12 et < 16, bonus de 0.2 point + Si la note est >= 16, bonus de 0.3 point + Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par + l'étudiant. + """ + + name = "bonus_iutva" + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # Calcule moyenne pondérée des notes de sport: + bonus_moy_gen_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_gen_arr[bonus_moy_gen_arr >= 10.0] = 0.1 + bonus_moy_gen_arr[bonus_moy_gen_arr >= 12.0] = 0.2 + bonus_moy_gen_arr[bonus_moy_gen_arr >= 16.0] = 0.3 + + # Bonus moyenne générale, et 0 sur les UE + self.bonus_moy_gen = pd.Series( + bonus_moy_gen_arr, index=self.etuds_idx, dtype=float + ) + if self.bonus_moy_gen_limit is not None: + # Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points + self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit) + + # Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs. + + +def get_bonus_class_dict(start=BonusSport, d=None): + """Dictionnaire des classes de bonus + (liste les sous-classes de BonusSport ayant un nom) + Resultat: { name : class } + """ + if d is None: + d = {} + if start.name != "virtual": + d[start.name] = start + for subclass in start.__subclasses__(): + get_bonus_class_dict(subclass, d=d) + return d diff --git a/app/comp/inscr_mod.py b/app/comp/inscr_mod.py index 8a5f4bc8..b9be1e9f 100644 --- a/app/comp/inscr_mod.py +++ b/app/comp/inscr_mod.py @@ -21,7 +21,7 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame: value: bool (0/1 inscrit ou pas) """ # méthode la moins lente: une requete par module, merge les dataframes - moduleimpl_ids = [m.id for m in formsemestre.modimpls] + moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted] etudids = [inscr.etudid for inscr in formsemestre.inscriptions] df = pd.DataFrame(index=etudids, dtype=int) for moduleimpl_id in moduleimpl_ids: @@ -47,10 +47,10 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame: def df_load_modimpl_inscr_v0(formsemestre): # methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente - moduleimpl_ids = [m.id for m in formsemestre.modimpls] + moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted] etudids = [i.etudid for i in formsemestre.inscriptions] df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool) - for modimpl in formsemestre.modimpls: + for modimpl in formsemestre.modimpls_sorted: ins_mod = df[modimpl.id] for inscr in modimpl.inscriptions: ins_mod[inscr.etudid] = True @@ -58,7 +58,7 @@ def df_load_modimpl_inscr_v0(formsemestre): def df_load_modimpl_inscr_v2(formsemestre): - moduleimpl_ids = [m.id for m in formsemestre.modimpls] + moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted] etudids = [i.etudid for i in formsemestre.inscriptions] df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool) cursor = db.engine.execute( diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 2fee521d..c3596f92 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -65,8 +65,9 @@ class ModuleImplResults: self.module_id = moduleimpl.module.id self.etudids = None "liste des étudiants inscrits au SEMESTRE" + self.nb_inscrits_module = None - "nombre d'inscrits (non DEM) au module" + "nombre d'inscrits (non DEM) à ce module" self.evaluations_completes = [] "séquence de booléens, indiquant les évals à prendre en compte." self.evaluations_completes_dict = {} @@ -263,14 +264,12 @@ class ModuleImplResultsAPC(ModuleImplResults): return self.etuds_moy_module -def load_evaluations_poids( - moduleimpl_id: int, default_poids=1.0 -) -> tuple[pd.DataFrame, list]: +def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: """Charge poids des évaluations d'un module et retourne un dataframe rows = evaluations, columns = UE, value = poids (float). Les valeurs manquantes (évaluations sans coef vers des UE) sont - remplies par default_poids. - Résultat: (evals_poids, liste de UE du semestre) + remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon. + Résultat: (evals_poids, liste de UEs du semestre sauf le sport) """ modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() @@ -282,8 +281,14 @@ def load_evaluations_poids( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids - if default_poids is not None: - evals_poids.fillna(value=default_poids, inplace=True) + # Initialise poids non enregistrés: + if np.isnan(evals_poids.values.flat).any(): + ue_coefs = modimpl.module.get_ue_coef_dict() + for ue in ues: + evals_poids[ue.id][evals_poids[ue.id].isna()] = ( + 1 if ue_coefs.get(ue.id, 0.0) > 0 else 0 + ) + return evals_poids, ues @@ -296,6 +301,7 @@ def moduleimpl_is_conforme( évaluations vers une UE de coefficient non nul est non nulle. Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs + NB: les UEs dans evals_poids sont sans le bonus sport """ nb_evals, nb_ues = evals_poids.shape if nb_evals == 0: diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 3c658988..ae167d4e 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -38,7 +38,7 @@ def compute_sem_moys_apc( = moyenne des moyennes d'UE, pondérée par la somme de leurs coefs etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid - modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE + modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE (sans ue bonus) Result: panda Series, index etudid, valeur float (moyenne générale) """ diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index ae8b98cc..cee1b888 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -62,6 +62,10 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data .filter( (Module.module_type == ModuleType.RESSOURCE) | (Module.module_type == ModuleType.SAE) + | ( + (Module.ue_id == UniteEns.id) + & (UniteEns.type == sco_codes_parcours.UE_SPORT) + ) ) .order_by( Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code @@ -102,13 +106,13 @@ def df_load_modimpl_coefs( et modules du formsemestre. Si ues et modimpls sont None, prend tous ceux du formsemestre. Résultat: (module_coefs_df, ues, modules) - DataFrame rows = UEs, columns = modimpl, value = coef. + DataFrame rows = UEs (avec bonus), columns = modimpl, value = coef. """ if ues is None: ues = formsemestre.query_ues().all() ue_ids = [x.id for x in ues] if modimpls is None: - modimpls = formsemestre.modimpls.all() + modimpls = formsemestre.modimpls_sorted modimpl_ids = [x.id for x in modimpls] mod2impl = {m.module.id: m.id for m in modimpls} modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float) @@ -134,7 +138,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray: assert len(modimpls_notes) modimpls_notes_arr = [df.values for df in modimpls_notes] modimpls_notes = np.stack(modimpls_notes_arr) - # passe de (mod x etud x ue) à (etud x mod x UE) + # passe de (mod x etud x ue) à (etud x mod x ue) return modimpls_notes.swapaxes(0, 1) @@ -144,10 +148,14 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: et assemble le cube. etuds: tous les inscrits au semestre (avec dem. et def.) - modimpls: _tous_ les modimpls de ce semestre - UEs: X?X voir quelles sont les UE considérées ici + modimpls: _tous_ les modimpls de ce semestre (y compris bonus sport) + UEs: toutes les UE du semestre (même si pas d'inscrits) SAUF le sport. - Resultat: + Attention: la liste des modimpls inclut les modules des UE sport, mais + elles ne sont pas dans la troisième dimension car elles n'ont pas de + "moyenne d'UE". + + Résultat: sem_cube : ndarray (etuds x modimpls x UEs) modimpls_evals_poids dict { modimpl.id : evals_poids } modimpls_results dict { modimpl.id : ModuleImplResultsAPC } @@ -155,7 +163,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple: modimpls_results = {} modimpls_evals_poids = {} modimpls_notes = [] - for modimpl in formsemestre.modimpls: + for modimpl in formsemestre.modimpls_sorted: mod_results = moy_mod.ModuleImplResultsAPC(modimpl) evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id) etuds_moy_module = mod_results.compute_module_moy(evals_poids) @@ -194,26 +202,27 @@ def compute_ue_moys_apc( modimpls : liste des modules à considérer (dim. 1 du cube) ues : liste des UE (dim. 2 du cube) modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) - modimpl_coefs_df: matrice coefficients (UE x modimpl) + modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport - Resultat: DataFrame columns UE, rows etudid + Résultat: DataFrame columns UE (sans sport), rows etudid """ - nb_etuds, nb_modules, nb_ues = sem_cube.shape + nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape + nb_ues_tot = len(ues) assert len(modimpls) == nb_modules if nb_modules == 0 or nb_etuds == 0: return pd.DataFrame( index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index ) assert len(etuds) == nb_etuds - assert len(ues) == nb_ues assert modimpl_inscr_df.shape[0] == nb_etuds assert modimpl_inscr_df.shape[1] == nb_modules - assert modimpl_coefs_df.shape[0] == nb_ues + assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus assert modimpl_coefs_df.shape[1] == nb_modules modimpl_inscr = modimpl_inscr_df.values modimpl_coefs = modimpl_coefs_df.values - # Duplique les inscriptions sur les UEs: - modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2) + + # Duplique les inscriptions sur les UEs non bonus: + modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2) # Enlève les NaN du numérateur: # si on veut prendre en compte les modules avec notes neutralisées ? sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0) @@ -234,7 +243,9 @@ def compute_ue_moys_apc( modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) return pd.DataFrame( - etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index + etud_moy_ue, + index=modimpl_inscr_df.index, # les etudids + columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport ) @@ -244,6 +255,7 @@ def compute_ue_moys_classic( ues: list, modimpl_inscr_df: pd.DataFrame, modimpl_coefs: np.array, + modimpl_mask: np.array, ) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]: """Calcul de la moyenne d'UE en mode classique. La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR @@ -251,13 +263,19 @@ def compute_ue_moys_classic( NA pas de notes disponibles ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici] - sem_matrix: notes moyennes aux modules + L'éventuel bonus sport n'est PAS appliqué ici. + + Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui + permet de sélectionner un sous-ensemble de modules (SAEs, tout sauf sport, ...). + + sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls) ndarray (etuds x modimpls) (floats avec des NaN) etuds : listes des étudiants (dim. 0 de la matrice) - ues : liste des UE + ues : liste des UE du semestre modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) modimpl_coefs: vecteur des coefficients de modules + modimpl_mask: masque des modimpls à prendre en compte Résultat: - moyennes générales: pd.Series, index etudid @@ -266,10 +284,15 @@ def compute_ue_moys_classic( les coefficients effectifs de chaque UE pour chaque étudiant (sommes de coefs de modules pris en compte) """ + # Restreint aux modules sélectionnés: + sem_matrix = sem_matrix[:, modimpl_mask] + modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask] + modimpl_coefs = modimpl_coefs[modimpl_mask] + nb_etuds, nb_modules = sem_matrix.shape assert len(modimpl_coefs) == nb_modules nb_ues = len(ues) - modimpl_inscr = modimpl_inscr_df.values + # Enlève les NaN du numérateur: sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0) # Ne prend pas en compte les notes des étudiants non inscrits au module: @@ -291,8 +314,8 @@ def compute_ue_moys_classic( etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index) # Calcul des moyennes d'UE ue_modules = np.array( - [[m.module.ue == ue for m in formsemestre.modimpls] for ue in ues] - )[..., np.newaxis] + [[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues] + )[..., np.newaxis][:, modimpl_mask, :] modimpl_coefs_etuds_no_nan_stacked = np.stack( [modimpl_coefs_etuds_no_nan.T] * nb_ues ) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 669380a3..2ae263d5 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -10,6 +10,9 @@ import pandas as pd 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.scodoc.sco_codes_parcours import UE_SPORT class ResultatsSemestreBUT(NotesTableCompat): @@ -37,26 +40,44 @@ class ResultatsSemestreBUT(NotesTableCompat): ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( - self.formsemestre, ues=self.ues, modimpls=self.modimpls + self.formsemestre, ues=self.ues, modimpls=self.formsemestre.modimpls_sorted ) # l'idx de la colonne du mod modimpl.id est # modimpl_coefs_df.columns.get_loc(modimpl.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) + + # Elimine les coefs des UE bonus sports + no_bonus = [ue.type != UE_SPORT for ue in self.ues] + modimpl_coefs_no_bonus_df = self.modimpl_coefs_df[no_bonus] self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.sem_cube, self.etuds, - self.modimpls, + self.formsemestre.modimpls_sorted, self.ues, self.modimpl_inscr_df, - self.modimpl_coefs_df, + modimpl_coefs_no_bonus_df, ) # 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 ) self.etud_moy_gen = moy_sem.compute_sem_moys_apc( - self.etud_moy_ue, self.modimpl_coefs_df + self.etud_moy_ue, modimpl_coefs_no_bonus_df ) + # --- Bonus Sport & Culture + bonus_class = ScoDocSiteConfig.get_bonus_sport_class() + if bonus_class is not None: + bonus: BonusSport = bonus_class( + self.formsemestre, + self.sem_cube, + self.ues, + self.modimpl_inscr_df, + self.modimpl_coefs_df.transpose(), + ) + self.bonus_ues = bonus.get_bonus_ues() + if self.bonus_ues is not None: + self.etud_moy_ue += self.bonus_ues # somme les dataframes + self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 68972ced..3c742867 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -11,7 +11,11 @@ import pandas as pd from app.comp import moy_mod, 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.formsemestre import FormSemestre +from app.scodoc.sco_codes_parcours import UE_SPORT +from app.scodoc.sco_utils import ModuleType class ResultatsSemestreClassic(NotesTableCompat): @@ -41,11 +45,20 @@ class ResultatsSemestreClassic(NotesTableCompat): ) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs = np.array( - [m.module.coefficient for m in self.formsemestre.modimpls] + [m.module.coefficient for m in self.formsemestre.modimpls_sorted] ) - self.modimpl_idx = {m.id: i for i, m in enumerate(self.formsemestre.modimpls)} + self.modimpl_idx = { + m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted) + } "l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]" + modimpl_standards_mask = np.array( + [ + (m.module.module_type == ModuleType.STANDARD) + and (m.module.ue.type != UE_SPORT) + for m in self.formsemestre.modimpls_sorted + ] + ) ( self.etud_moy_gen, self.etud_moy_ue, @@ -56,7 +69,28 @@ class ResultatsSemestreClassic(NotesTableCompat): self.ues, self.modimpl_inscr_df, self.modimpl_coefs, + modimpl_standards_mask, ) + # --- Bonus Sport & Culture + bonus_class = ScoDocSiteConfig.get_bonus_sport_class() + if bonus_class is not None: + bonus: BonusSport = bonus_class( + self.formsemestre, + self.sem_matrix, + self.ues, + self.modimpl_inscr_df, + self.modimpl_coefs, + ) + self.bonus_ues = bonus.get_bonus_ues() + if self.bonus_ues is not None: + self.etud_moy_ue += self.bonus_ues # somme les dataframes + bonus_mg = bonus.get_bonus_moy_gen() + if bonus_mg is not None: + self.etud_moy_gen += bonus_mg + self.bonus = ( + bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins + ) + # --- Classements: self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: @@ -85,9 +119,9 @@ class ResultatsSemestreClassic(NotesTableCompat): } -def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple: +def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]: """Calcule la matrice des notes du semestre - (charge toutes les notes, calcule les moyenne des modules + (charge toutes les notes, calcule les moyennes des modules et assemble la matrice) Resultat: sem_matrix : 2d-array (etuds x modimpls) @@ -95,7 +129,7 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple: """ modimpls_results = {} modimpls_notes = [] - for modimpl in formsemestre.modimpls: + for modimpl in formsemestre.modimpls_sorted: mod_results = moy_mod.ModuleImplResultsClassic(modimpl) etuds_moy_module = mod_results.compute_module_moy() modimpls_results[modimpl.id] = mod_results diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 1ff65468..9356bf89 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -100,42 +100,28 @@ class ResultatsSemestre: @cached_property def ues(self) -> list[UniteEns]: - """Liste des UEs du semestre + """Liste des UEs du semestre (avec les UE bonus sport) (indices des DataFrames) """ return self.formsemestre.query_ues(with_sport=True).all() - @cached_property - def modimpls(self): - """Liste des modimpls du semestre - - triée par numéro de module en APC - - triée par numéros d'UE/matières/modules pour les formations standard. - """ - modimpls = self.formsemestre.modimpls.all() - if self.is_apc: - modimpls.sort(key=lambda m: (m.module.numero, m.module.code)) - else: - modimpls.sort( - key=lambda m: ( - m.module.ue.numero, - m.module.matiere.numero, - m.module.numero, - m.module.code, - ) - ) - return modimpls - @cached_property def ressources(self): "Liste des ressources du semestre, triées par numéro de module" return [ - m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE + m + for m in self.formsemestre.modimpls_sorted + if m.module.module_type == scu.ModuleType.RESSOURCE ] @cached_property def saes(self): "Liste des SAÉs du semestre, triées par numéro de module" - return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE] + return [ + m + for m in self.formsemestre.modimpls_sorted + if m.module.module_type == scu.ModuleType.SAE + ] @cached_property def ue_validables(self) -> list: @@ -163,16 +149,20 @@ class NotesTableCompat(ResultatsSemestre): développements (API malcommode et peu efficace). """ - _cached_attrs = ResultatsSemestre._cached_attrs + () + _cached_attrs = ResultatsSemestre._cached_attrs + ( + "bonus", + "bonus_ues", + ) def __init__(self, formsemestre: FormSemestre): super().__init__(formsemestre) nb_etuds = len(self.etuds) - self.bonus = defaultdict(lambda: 0.0) # XXX TODO - self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues} + self.bonus = None # virtuel + self.bonus_ues = None # virtuel + self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues} self.mod_rangs = { - m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls + m.id: (None, nb_etuds) for m in self.formsemestre.modimpls_sorted } self.moy_min = "NA" self.moy_max = "NA" @@ -221,7 +211,11 @@ class NotesTableCompat(ResultatsSemestre): ues = [] for ue in self.formsemestre.query_ues(with_sport=not filter_sport): d = ue.to_dict() - d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).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 @@ -230,9 +224,13 @@ class NotesTableCompat(ResultatsSemestre): triés par numéros (selon le type de formation) """ if ue_id is None: - return [m.to_dict() for m in self.modimpls] + return [m.to_dict() for m in self.formsemestre.modimpls_sorted] else: - return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id] + return [ + m.to_dict() + for m in self.formsemestre.modimpls_sorted + if m.module.ue.id == ue_id + ] def get_etud_decision_sem(self, etudid: int) -> dict: """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu. @@ -359,12 +357,16 @@ class NotesTableCompat(ResultatsSemestre): moy_gen = self.etud_moy_gen.get(etudid, False) if moy_gen is False: # pas de moyenne: démissionnaire ou def - t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls) + t = ( + ["-"] + + ["0.00"] * len(self.ues) + + ["NI"] * len(self.formsemestre.modimpls_sorted) + ) else: moy_ues = self.etud_moy_ue.loc[etudid] t = [moy_gen] + list(moy_ues) # TODO UE capitalisées: ne pas afficher moyennes modules - for modimpl in self.modimpls: + for modimpl in self.formsemestre.modimpls_sorted: val = self.get_etud_mod_moy(modimpl.id, etudid) t.append(val) t.append(etudid) diff --git a/app/forms/main/config_forms.py b/app/forms/main/config_forms.py index 16be8451..26548f08 100644 --- a/app/forms/main/config_forms.py +++ b/app/forms/main/config_forms.py @@ -310,7 +310,7 @@ class ScoDocConfigurationForm(FlaskForm): label="Fonction de calcul des bonus sport&culture", choices=[ (x, x if x else "Aucune") - for x in ScoDocSiteConfig.get_bonus_sport_func_names() + for x in ScoDocSiteConfig.get_bonus_sport_class_names() ], ) depts = FieldList(FormField(DeptForm)) @@ -363,7 +363,7 @@ class ScoDocConfigurationForm(FlaskForm): def select_action(self): if ( self.data["bonus_sport_func_name"] - != ScoDocSiteConfig.get_bonus_sport_func_name() + != ScoDocSiteConfig.get_bonus_sport_class_name() ): return BonusSportUpdate(self.data) for dept_entry in self.depts: @@ -381,7 +381,7 @@ def configuration(): raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) form = ScoDocConfigurationForm( data=_make_data( - bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(), + bonus_sport=ScoDocSiteConfig.get_bonus_sport_class_name(), modele=sco_logos.list_logos(), ) ) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index a74f1671..e1febc32 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -112,6 +112,9 @@ class FormSemestre(db.Model): if self.modalite is None: self.modalite = FormationModalite.DEFAULT_MODALITE + def __repr__(self): + return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>" + def to_dict(self): d = dict(self.__dict__) d.pop("_sa_instance_state", None) @@ -152,6 +155,28 @@ class FormSemestre(db.Model): sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) return sem_ues.order_by(UniteEns.numero) + @cached_property + def modimpls_sorted(self) -> list[ModuleImpl]: + """Liste des modimpls du semestre + - triée par type/numéro/code en APC + - triée par numéros d'UE/matières/modules pour les formations standard. + """ + modimpls = self.modimpls.all() + if self.formation.is_apc(): + modimpls.sort( + key=lambda m: (m.module.module_type, m.module.numero, m.module.code) + ) + else: + modimpls.sort( + key=lambda m: ( + m.module.ue.numero, + m.module.matiere.numero, + m.module.numero, + m.module.code, + ) + ) + return modimpls + def est_courant(self) -> bool: """Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 2aa36da9..d51a620b 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -5,7 +5,7 @@ import pandas as pd from app import db from app.comp import df_cache -from app.models import UniteEns, Identite +from app.models import Identite, Module import app.scodoc.notesdb as ndb from app.scodoc import sco_utils as scu @@ -127,3 +127,16 @@ class ModuleImplInscription(db.Model): ModuleImpl, backref=db.backref("inscriptions", cascade="all, delete-orphan"), ) + + @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 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() diff --git a/app/models/modules.py b/app/models/modules.py index 24e55246..ac82b127 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -4,6 +4,7 @@ from app import db from app.models import APO_CODE_STR_LEN from app.scodoc import sco_utils as scu +from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -131,7 +132,8 @@ class Module(db.Model): def ue_coefs_list(self, include_zeros=True): """Liste des coefs vers les UE (pour les modules APC). - Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre. + Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre, + sauf UE bonus sport. Result: List of tuples [ (ue, coef) ] """ if not self.is_apc(): @@ -140,6 +142,7 @@ class Module(db.Model): # Toutes les UE du même semestre: ues_semestre = ( self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx) + .filter(UniteEns.type != UE_SPORT) .order_by(UniteEns.numero) .all() ) diff --git a/app/models/preferences.py b/app/models/preferences.py index 59c82ec8..f220ee17 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -3,7 +3,7 @@ """Model : preferences """ from app import db, log -from app.scodoc import bonus_sport +from app.comp import bonus_spo from app.scodoc.sco_exceptions import ScoValueError @@ -61,47 +61,80 @@ class ScoDocSiteConfig(db.Model): } @classmethod - def set_bonus_sport_func(cls, func_name): + def set_bonus_sport_class(cls, class_name): """Record bonus_sport config. - If func_name not defined, raise NameError + If class_name not defined, raise NameError """ - if func_name not in cls.get_bonus_sport_func_names(): - raise NameError("invalid function name for bonus_sport") + if class_name not in cls.get_bonus_sport_class_names(): + raise NameError("invalid class name for bonus_sport") c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() if c: - log("setting to " + func_name) - c.value = func_name + log("setting to " + class_name) + c.value = class_name else: - c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name) + c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name) db.session.add(c) db.session.commit() @classmethod - def get_bonus_sport_func_name(cls): + def get_bonus_sport_class_name(cls): """Get configured bonus function name, or None if None.""" - f = cls.get_bonus_sport_func_from_name() - if f is None: + klass = cls.get_bonus_sport_class_from_name() + if klass is None: return "" else: - return f.__name__ + return klass.name + + @classmethod + def get_bonus_sport_class(cls): + """Get configured bonus function, or None if None.""" + return cls.get_bonus_sport_class_from_name() + + @classmethod + def get_bonus_sport_class_from_name(cls, class_name=None): + """returns bonus class with specified name. + If name not specified, return the configured function. + None if no bonus function configured. + Raises ScoValueError if class_name not found in module bonus_sport. + """ + if class_name is None: + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c is None: + return None + class_name = c.value + if class_name == "": # pas de bonus défini + return None + klass = bonus_spo.get_bonus_class_dict().get(class_name) + if klass is None: + raise ScoValueError( + f"""Fonction de calcul bonus sport inexistante: {class_name}. + (contacter votre administrateur local).""" + ) + return klass + + @classmethod + def get_bonus_sport_class_names(cls): + """List available functions names + (starting with empty string to represent "no bonus function"). + """ + return [""] + sorted(bonus_spo.get_bonus_class_dict().keys()) @classmethod def get_bonus_sport_func(cls): - """Get configured bonus function, or None if None.""" - return cls.get_bonus_sport_func_from_name() - - @classmethod - def get_bonus_sport_func_from_name(cls, func_name=None): + """Fonction bonus_sport ScoDoc 7 XXX + Transitoire pour les tests durant la transition #sco92 + """ """returns bonus func with specified name. If name not specified, return the configured function. None if no bonus function configured. Raises ScoValueError if func_name not found in module bonus_sport. """ - if func_name is None: - c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() - if c is None: - return None - func_name = c.value + from app.scodoc import bonus_sport + + c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() + if c is None: + return None + func_name = c.value if func_name == "": # pas de bonus défini return None try: @@ -111,16 +144,3 @@ class ScoDocSiteConfig(db.Model): f"""Fonction de calcul maison inexistante: {func_name}. (contacter votre administrateur local).""" ) - - @classmethod - def get_bonus_sport_func_names(cls): - """List available functions names - (starting with empty string to represent "no bonus function"). - """ - return [""] + sorted( - [ - getattr(bonus_sport, name).__name__ - for name in dir(bonus_sport) - if name.startswith("bonus_") - ] - ) diff --git a/app/models/ues.py b/app/models/ues.py index 26223eef..3497414c 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -41,6 +41,8 @@ class UniteEns(db.Model): # coef UE, utilise seulement si l'option use_ue_coefs est activée: coefficient = db.Column(db.Float) + color = db.Column(db.Text()) + # relations matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue") diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index 722b7ec9..23b50039 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -73,7 +73,8 @@ def TrivialFormulator( input_type : 'text', 'textarea', 'password', 'radio', 'menu', 'checkbox', 'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation), - 'boolcheckbox', 'text_suggest' + 'boolcheckbox', 'text_suggest', + 'color' (default text) size : text field width rows, cols: textarea geometry @@ -594,6 +595,11 @@ class TF(object): var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts); """ ) + elif input_type == "color": + lem.append( + '') % values) else: raise ValueError("unkown input_type for form (%s)!" % input_type) explanation = descr.get("explanation", "") @@ -712,7 +718,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts); R.append("%s" % title) R.append('' % klass) - if input_type == "text" or input_type == "text_suggest": + if ( + input_type == "text" + or input_type == "text_suggest" + or input_type == "color" + ): R.append(("%(" + field + ")s") % self.values) elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"): if input_type == "boolcheckbox": diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 75b08b50..5351ecb3 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -77,7 +77,6 @@ def bonus_iutv(notes_sport, coefs, infos=None): optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant. """ - # breakpoint() bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10]) return bonus @@ -91,7 +90,7 @@ def bonus_direct(notes_sport, coefs, infos=None): def bonus_iut_stdenis(notes_sport, coefs, infos=None): - """Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.""" + """Semblable à bonus_iutv mais total limité à 0.5 points.""" points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10 bonus = points * 0.05 # ou / 20 return min(bonus, 0.5) # bonus limité à 1/2 point diff --git a/app/scodoc/htmlutils.py b/app/scodoc/htmlutils.py index 3306b80d..65101f1f 100644 --- a/app/scodoc/htmlutils.py +++ b/app/scodoc/htmlutils.py @@ -29,6 +29,7 @@ """ from html.parser import HTMLParser from html.entities import name2codepoint +from multiprocessing.sharedctypes import Value import re from flask import g, url_for @@ -36,17 +37,23 @@ from flask import g, url_for from . import listhistogram -def horizontal_bargraph(value, mark): +def horizontal_bargraph(value, mark) -> str: """html drawing an horizontal bar and a mark used to vizualize the relative level of a student """ - tmpl = """ + try: + vals = {"value": int(value), "mark": int(mark)} + except ValueError: + return "" + return ( + """ """ - return tmpl % {"value": int(value), "mark": int(mark)} + % vals + ) def histogram_notes(notes): diff --git a/app/scodoc/notes_table.py b/app/scodoc/notes_table.py index eec178f1..090f18e0 100644 --- a/app/scodoc/notes_table.py +++ b/app/scodoc/notes_table.py @@ -170,7 +170,7 @@ class NotesTable: """ def __init__(self, formsemestre_id): - log(f"NotesTable( formsemestre_id={formsemestre_id} )") + # log(f"NotesTable( formsemestre_id={formsemestre_id} )") # raise NotImplementedError() # XXX if not formsemestre_id: raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id) @@ -909,6 +909,7 @@ class NotesTable: if len(coefs_bonus_gen) == 1: coefs_bonus_gen = [1.0] # irrelevant, may be zero + # XXX attention: utilise anciens bonus_sport, évidemment bonus_func = ScoDocSiteConfig.get_bonus_sport_func() if bonus_func: bonus = bonus_func( diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 1941d051..4dcf4d81 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -43,6 +43,7 @@ from flask import g, request from flask import url_for from flask_login import current_user from flask_mail import Message +from app.models.moduleimpls import ModuleImplInscription import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -285,19 +286,29 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): else: I["rang_nt"], I["rang_txt"] = "", "" I["note_max"] = 20.0 # notes toujours sur 20 - I["bonus_sport_culture"] = nt.bonus[etudid] + I["bonus_sport_culture"] = nt.bonus[etudid] if nt.bonus is not None else 0.0 # Liste les UE / modules /evals I["ues"] = [] I["matieres_modules"] = {} I["matieres_modules_capitalized"] = {} for ue in ues: + if ( + ModuleImplInscription.nb_inscriptions_dans_ue( + formsemestre_id, etudid, ue["ue_id"] + ) + == 0 + ): + 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"]) else: - x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True) + if nt.bonus is not None: + x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True) + else: + x = "" if isinstance(x, str): u["cur_moy_ue_txt"] = "pas de bonus" else: diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 5a6d542d..89f65504 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -192,7 +192,9 @@ def formsemestre_bulletinetud_published_dict( ) d["note_max"] = dict(value=20) # notes toujours sur 20 - d["bonus_sport_culture"] = dict(value=nt.bonus[etudid]) + d["bonus_sport_culture"] = dict( + value=nt.bonus[etudid] if nt.bonus is not None else 0.0 + ) # Liste les UE / modules /evals d["ue"] = [] diff --git a/app/scodoc/sco_bulletins_xml.py b/app/scodoc/sco_bulletins_xml.py index aa2b9577..5d6ba7de 100644 --- a/app/scodoc/sco_bulletins_xml.py +++ b/app/scodoc/sco_bulletins_xml.py @@ -195,7 +195,12 @@ def make_xml_formsemestre_bulletinetud( ) ) doc.append(Element("note_max", value="20")) # notes toujours sur 20 - doc.append(Element("bonus_sport_culture", value=str(nt.bonus[etudid]))) + doc.append( + Element( + "bonus_sport_culture", + value=str(nt.bonus[etudid] if nt.bonus is not None else 0.0), + ) + ) # Liste les UE / modules /evals for ue in ues: ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) @@ -211,7 +216,7 @@ def make_xml_formsemestre_bulletinetud( if ue["type"] != sco_codes_parcours.UE_SPORT: v = ue_status["cur_moy_ue"] else: - v = nt.bonus[etudid] + v = nt.bonus[etudid] if nt.bonus is not None else 0.0 x_ue.append( Element( "note", diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 17ddd09b..59ebab2d 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -98,8 +98,9 @@ class ScoDocCache: status = CACHE.set(key, value, timeout=cls.timeout) if not status: log("Error: cache set failed !") - except: + except Exception as exc: log("XXX CACHE Warning: error in set !!!") + log(exc) status = None return status diff --git a/app/scodoc/sco_config_actions.py b/app/scodoc/sco_config_actions.py index c3ccc9cb..949567ec 100644 --- a/app/scodoc/sco_config_actions.py +++ b/app/scodoc/sco_config_actions.py @@ -168,7 +168,7 @@ class BonusSportUpdate(Action): def build_action(parameters): if ( parameters["bonus_sport_func_name"] - != ScoDocSiteConfig.get_bonus_sport_func_name() + != ScoDocSiteConfig.get_bonus_sport_class_name() ): return [BonusSportUpdate(parameters)] return [] diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 6b788459..5282c5ce 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -81,6 +81,7 @@ _ueEditor = ndb.EditableTable( "is_external", "code_apogee", "coefficient", + "color", ), sortkey="numero", input_formators={ @@ -358,6 +359,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None): "explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement", }, ), + ( + "color", + { + "input_type": "color", + "title": "Couleur", + "explanation": "pour affichages", + }, + ), ] if create and not parcours.UE_IS_MODULE and not is_apc: fw.append( diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index d0ee4b1a..2c5a8af4 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1107,6 +1107,7 @@ _TABLEAU_MODULES_HEAD = """ Module Inscrits Responsable +Coefs. Évaluations """ @@ -1213,7 +1214,21 @@ def formsemestre_tableau_modules( sco_users.user_info(modimpl["responsable_id"])["prenomnom"], ) ) - + H.append("") + if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): + coefs = mod.ue_coefs_list() + for coef in coefs: + if coef[1] > 0: + H.append( + f"""""" + ) + else: + H.append(f"""""") + H.append("") if mod.module_type in ( None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs ModuleType.STANDARD, diff --git a/app/scodoc/sco_liste_notes.py b/app/scodoc/sco_liste_notes.py index 4cd76888..a342e312 100644 --- a/app/scodoc/sco_liste_notes.py +++ b/app/scodoc/sco_liste_notes.py @@ -37,7 +37,10 @@ from app.models.moduleimpls import ModuleImpl import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log +from app.comp import res_sem from app.comp import moy_mod +from app.comp.moy_mod import ModuleImplResults +from app.comp.res_common import NotesTableCompat from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc import sco_cache from app.scodoc import sco_edit_module @@ -432,7 +435,7 @@ def _make_table_notes( if is_apc: # Ajoute une colonne par UE _add_apc_columns( - moduleimpl_id, + modimpl, evals_poids, ues, rows, @@ -815,7 +818,7 @@ def _add_moymod_column( def _add_apc_columns( - moduleimpl_id, + modimpl, evals_poids, ues, rows, @@ -834,18 +837,23 @@ def _add_apc_columns( # => On recharge tout dans les nouveaux modèles # rows est une liste de dict avec une clé "etudid" # on va y ajouter une clé par UE du semestre - modimpl = ModuleImpl.query.get(moduleimpl_id) - evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( - moduleimpl_id - ) - etuds_moy_module = moy_mod.compute_module_moy( - evals_notes, evals_poids, evaluations, evaluations_completes - ) + nt: NotesTableCompat = res_sem.load_formsemestre_result(modimpl.formsemestre) + modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id] + + # XXX A ENLEVER TODO + # modimpl = ModuleImpl.query.get(moduleimpl_id) + + # evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes( + # moduleimpl_id + # ) + # etuds_moy_module = moy_mod.compute_module_moy( + # evals_notes, evals_poids, evaluations, evaluations_completes + # ) if is_conforme: # valeur des moyennes vers les UEs: for row in rows: for ue in ues: - moy_ue = etuds_moy_module[ue.id].get(row["etudid"], "?") + moy_ue = modimpl_results.etuds_moy_module[ue.id].get(row["etudid"], "?") row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric) row[f"_moy_ue_{ue.id}_class"] = "moy_ue" # Nom et coefs des UE (lignes titres): diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 904c74c9..af2e15dd 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -171,7 +171,9 @@ def _ue_coefs_html(coefs_lst) -> str: """ + "\n".join( [ - f"""
{coef}
{ue.acronyme}
""" + f"""
{coef}
{ue.acronyme}
""" for ue, coef in coefs_lst ] ) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index ecb4f3b9..d91ce733 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1308,6 +1308,20 @@ td.formsemestre_status_cell { white-space: nowrap; } +span.mod_coef_indicator, span.ue_color_indicator { + display:inline-block; + width: 10px; + height: 10px; +} +span.mod_coef_indicator_zero { + display:inline-block; + width: 9px; + height: 9px; + border: 1px solid rgb(156, 156, 156); +} + + + span.status_ue_acro { font-weight: bold; } span.status_ue_title { font-style: italic; padding-left: 1cm;} span.status_module_cat { font-weight: bold; } diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index 8700116c..d6b1fe12 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -30,6 +30,8 @@ }}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %} + {{ue.acronyme}} {{ue.titre}} diff --git a/migrations/versions/c95d5a3bf0de_couleur_ue.py b/migrations/versions/c95d5a3bf0de_couleur_ue.py new file mode 100644 index 00000000..f4dc60fc --- /dev/null +++ b/migrations/versions/c95d5a3bf0de_couleur_ue.py @@ -0,0 +1,28 @@ +"""couleur UE + +Revision ID: c95d5a3bf0de +Revises: f40fbaf5831c +Create Date: 2022-01-24 21:44:55.205544 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "c95d5a3bf0de" +down_revision = "f40fbaf5831c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("notes_ue", sa.Column("color", sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("notes_ue", "color") + # ### end Alembic commands ### diff --git a/scodoc.py b/scodoc.py index 3db205e4..cba555fc 100755 --- a/scodoc.py +++ b/scodoc.py @@ -133,7 +133,7 @@ def user_create(username, role, dept, nom=None, prenom=None): # user-create "Create a new user" r = Role.get_named_role(role) if not r: - sys.stderr.write("user_create: role {r} does not exists\n".format(r=role)) + sys.stderr.write("user_create: role {r} does not exist\n".format(r=role)) return 1 u = User.query.filter_by(user_name=username).first() if u: From 4133b9df6a0f67a71f6a5af6bd8a9668b3f7c815 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 25 Jan 2022 13:24:08 +0100 Subject: [PATCH 24/70] =?UTF-8?q?R=C3=A9glage=20publication=20bulletin=20J?= =?UTF-8?q?SON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 11 +++++++++-- app/scodoc/sco_bulletins_json.py | 4 ++-- app/views/notes.py | 5 ++++- sco_version.py | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index eac6d52c..1c45d6e0 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -219,13 +219,18 @@ class ResultatsSemestreBUT: } return d - def bulletin_etud(self, etud, formsemestre) -> dict: - """Le bulletin de l'étudiant dans ce semestre""" + def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict: + """Le bulletin de l'étudiant dans ce semestre. + Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai + (bulletins non publiés). + """ etat_inscription = etud.etat_inscription(formsemestre.id) + published = (not formsemestre.bul_hide_xml) or force_publishing d = { "version": "0", "type": "BUT", "date": datetime.datetime.utcnow().isoformat() + "Z", + "publie": not formsemestre.bul_hide_xml, "etudiant": etud.to_dict_bul(), "formation": { "id": formsemestre.formation.id, @@ -237,6 +242,8 @@ class ResultatsSemestreBUT: "etat_inscription": etat_inscription, "options": bulletin_option_affichage(formsemestre), } + if not published: + return d semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "date_debut": formsemestre.date_debut.isoformat(), diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index c3812e74..cb31ed14 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -98,9 +98,9 @@ def formsemestre_bulletinetud_published_dict( d = {} if (not sem["bul_hide_xml"]) or force_publishing: - published = 1 + published = True else: - published = 0 + published = False if xml_nodate: docdate = "" else: diff --git a/app/views/notes.py b/app/views/notes.py index 4be64afb..c9da6f64 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -307,7 +307,9 @@ def formsemestre_bulletinetud( ) if format == "json": r = bulletin_but.ResultatsSemestreBUT(formsemestre) - return jsonify(r.bulletin_etud(etud, formsemestre)) + return jsonify( + r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing) + ) elif format == "html": return render_template( "but/bulletin.html", @@ -318,6 +320,7 @@ def formsemestre_bulletinetud( formsemestre_id=formsemestre_id, etudid=etudid, format="json", + force_publishing=1, # pour ScoDoc lui même ), sco=ScoData(), ) diff --git a/sco_version.py b/sco_version.py index 55a2c785..9540dbe5 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.32" +SCOVERSION = "9.1.33" SCONAME = "ScoDoc" From 417cb53603917eb5621fb6140dfc2d3130317007 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 25 Jan 2022 22:18:49 +0100 Subject: [PATCH 25/70] corrections post-merge --- app/models/__init__.py | 2 +- app/models/config.py | 5 +++++ app/models/formsemestre.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index 25ac6c11..f1108493 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -12,7 +12,6 @@ GROUPNAME_STR_LEN = 64 from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence -from app.models.config import ScoDocSiteConfig from app.models.departements import Departement from app.models.etudiants import ( Identite, @@ -65,3 +64,4 @@ from app.models.but_refcomp import ( ApcSituationPro, ApcAppCritique, ) +from app.models.config import ScoDocSiteConfig diff --git a/app/models/config.py b/app/models/config.py index f9345942..c69b5ae6 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -110,6 +110,11 @@ class ScoDocSiteConfig(db.Model): else: return klass.name + @classmethod + def get_bonus_sport_class(cls): + """Get configured bonus function, or None if None.""" + return cls.get_bonus_sport_class_from_name() + @classmethod def get_bonus_sport_class_from_name(cls, class_name=None): """returns bonus class with specified name. diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 4d436f8e..a6016031 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -49,7 +49,7 @@ class FormSemestre(db.Model): gestion_compensation = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) - # ne publie pas le bulletin XML: + # ne publie pas le bulletin XML ou JSON: bul_hide_xml = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) From 08e9d2449a4708b25e800ac9b8d00892fa8b3e6d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 25 Jan 2022 23:48:39 +0100 Subject: [PATCH 26/70] Reorganisation form configuration globale. Doc bonus. --- app/forms/main/config_apo.py | 1 - .../main/{config_forms.py => config_logos.py} | 35 ++--- app/forms/main/config_main.py | 76 +++++++++++ app/scodoc/sco_config_actions.py | 25 ---- app/scodoc/sco_preferences.py | 2 +- .../js/{configuration.js => config_logos.js} | 2 +- app/templates/config_logos.html | 125 +++++++++++++++++ app/templates/configuration.html | 128 +++++------------- app/views/scodoc.py | 24 +++- scodoc.py | 1 - 10 files changed, 267 insertions(+), 152 deletions(-) rename app/forms/main/{config_forms.py => config_logos.py} (92%) create mode 100644 app/forms/main/config_main.py rename app/static/js/{configuration.js => config_logos.js} (54%) create mode 100644 app/templates/config_logos.html diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index a655f450..6151f2f3 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -28,7 +28,6 @@ """ Formulaires configuration Exports Apogée (codes) """ -import re from flask import flash, url_for, redirect, render_template from flask_wtf import FlaskForm diff --git a/app/forms/main/config_forms.py b/app/forms/main/config_logos.py similarity index 92% rename from app/forms/main/config_forms.py rename to app/forms/main/config_logos.py index 26548f08..91a73747 100644 --- a/app/forms/main/config_forms.py +++ b/app/forms/main/config_logos.py @@ -47,7 +47,6 @@ from app.scodoc.sco_config_actions import ( LogoDelete, LogoUpdate, LogoInsert, - BonusSportUpdate, ) from flask_login import current_user @@ -296,23 +295,15 @@ def _make_depts_data(modele): return data -def _make_data(bonus_sport, modele): +def _make_data(modele): data = { - "bonus_sport_func_name": bonus_sport, "depts": _make_depts_data(modele=modele), } return data -class ScoDocConfigurationForm(FlaskForm): - "Panneau de configuration général" - bonus_sport_func_name = SelectField( - label="Fonction de calcul des bonus sport&culture", - choices=[ - (x, x if x else "Aucune") - for x in ScoDocSiteConfig.get_bonus_sport_class_names() - ], - ) +class LogosConfigurationForm(FlaskForm): + "Panneau de configuration des logos" depts = FieldList(FormField(DeptForm)) def __init__(self, *args, **kwargs): @@ -361,11 +352,6 @@ class ScoDocConfigurationForm(FlaskForm): return dept_form.get_form(logoname) def select_action(self): - if ( - self.data["bonus_sport_func_name"] - != ScoDocSiteConfig.get_bonus_sport_class_name() - ): - return BonusSportUpdate(self.data) for dept_entry in self.depts: dept_form = dept_entry.form action = dept_form.select_action() @@ -374,14 +360,11 @@ class ScoDocConfigurationForm(FlaskForm): return None -def configuration(): - """Panneau de configuration général""" - auth_name = str(current_user) - if not current_user.is_administrator(): - raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) - form = ScoDocConfigurationForm( +def config_logos(): + "Page de configuration des logos" + # nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue + form = LogosConfigurationForm( data=_make_data( - bonus_sport=ScoDocSiteConfig.get_bonus_sport_class_name(), modele=sco_logos.list_logos(), ) ) @@ -392,11 +375,11 @@ def configuration(): flash(action.message) return redirect( url_for( - "scodoc.configuration", + "scodoc.configure_logos", ) ) return render_template( - "configuration.html", + "config_logos.html", scodoc_dept=None, title="Configuration ScoDoc", form=form, diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py new file mode 100644 index 00000000..e0b349e4 --- /dev/null +++ b/app/forms/main/config_main.py @@ -0,0 +1,76 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +""" +Formulaires configuration Exports Apogée (codes) +""" + +from flask import flash, url_for, redirect, request, render_template +from flask_wtf import FlaskForm +from wtforms import SelectField, SubmitField + +import app +from app.models import ScoDocSiteConfig + + +class ScoDocConfigurationForm(FlaskForm): + "Panneau de configuration des logos" + bonus_sport_func_name = SelectField( + label="Fonction de calcul des bonus sport&culture", + choices=[ + (x, x if x else "Aucune") + for x in ScoDocSiteConfig.get_bonus_sport_class_names() + ], + ) + submit = SubmitField("Valider") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) + + +def configuration(): + "Page de configuration principale" + # nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue + form = ScoDocConfigurationForm( + data={ + "bonus_sport_func_name": ScoDocSiteConfig.get_bonus_sport_class_name(), + } + ) + if request.method == "POST" and form.cancel.data: # cancel button + return redirect(url_for("scodoc.index")) + if form.validate_on_submit(): + if ( + form.data["bonus_sport_func_name"] + != ScoDocSiteConfig.get_bonus_sport_class_name() + ): + ScoDocSiteConfig.set_bonus_sport_class(form.data["bonus_sport_func_name"]) + app.clear_scodoc_cache() + flash(f"Fonction bonus sport&culture configurée.") + return redirect(url_for("scodoc.index")) + + return render_template( + "configuration.html", + form=form, + ) diff --git a/app/scodoc/sco_config_actions.py b/app/scodoc/sco_config_actions.py index 949567ec..f6ea9637 100644 --- a/app/scodoc/sco_config_actions.py +++ b/app/scodoc/sco_config_actions.py @@ -152,28 +152,3 @@ class LogoInsert(Action): name=self.parameters["name"], dept_id=dept_id, ) - - -class BonusSportUpdate(Action): - """Action: Change bonus_sport_function_name. - bonus_sport_function_name: the new value""" - - def __init__(self, parameters): - super().__init__( - f"Changement du calcul de bonus sport pour ({parameters['bonus_sport_func_name']}).", - parameters, - ) - - @staticmethod - def build_action(parameters): - if ( - parameters["bonus_sport_func_name"] - != ScoDocSiteConfig.get_bonus_sport_class_name() - ): - return [BonusSportUpdate(parameters)] - return [] - - def execute(self): - current_app.logger.info(self.message) - ScoDocSiteConfig.set_bonus_sport_func(self.parameters["bonus_sport_func_name"]) - app.clear_scodoc_cache() diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index 6aba9694..dbbdd0ac 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -2019,7 +2019,7 @@ class BasePreferences(object): H = [ html_sco_header.sco_header(page_title="Préférences"), "

Préférences globales pour %s

" % scu.ScoURL(), - # f"""

modification des logos du département (pour documents pdf)

""" # if current_user.is_administrator() # else "", diff --git a/app/static/js/configuration.js b/app/static/js/config_logos.js similarity index 54% rename from app/static/js/configuration.js rename to app/static/js/config_logos.js index b537d572..56b7cd27 100644 --- a/app/static/js/configuration.js +++ b/app/static/js/config_logos.js @@ -1,5 +1,5 @@ function submit_form() { - $("#configuration_form").submit(); + $("#config_logos_form").submit(); } $(function () { diff --git a/app/templates/config_logos.html b/app/templates/config_logos.html new file mode 100644 index 00000000..f4bd543c --- /dev/null +++ b/app/templates/config_logos.html @@ -0,0 +1,125 @@ +{# -*- mode: jinja-html -*- #} +{% extends 'base.html' %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% macro render_field(field, with_label=True) %} +
+ {% if with_label %} + {{ field.label }} : + {% endif %} + {{ field(**kwargs)|safe }} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+
+{% endmacro %} + +{% macro render_add_logo(add_logo_form) %} +
+

Ajouter un logo

+ {{ add_logo_form.hidden_tag() }} + {{ render_field(add_logo_form.name) }} + {{ render_field(add_logo_form.upload) }} + {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} +
+{% endmacro %} + +{% macro render_logo(dept_form, logo_form) %} +
+ {{ logo_form.hidden_tag() }} + {% if logo_form.titre %} + + +
+

{{ logo_form.titre }}

+
+
{{ logo_form.description or "" }}
+ + + {% else %} + + + +

Logo personalisé: {{ logo_form.logo_id.data }}

+
+ {{ logo_form.description or "" }} + + + {% endif %} + + +
+ pas de logo chargé +
+ + +

{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})

+ Taille: {{ logo_form.logo.size }} px + {% if logo_form.logo.mm %}   /   {{ logo_form.logo.mm }} mm {% endif %}
+ Aspect ratio: {{ logo_form.logo.aspect_ratio }}
+ Usage: {{ logo_form.logo.get_usage() }} + + +

Modifier l'image

+ {{ render_field(logo_form.upload, False, onchange="submit_form()") }} + {% if logo_form.can_delete %} +

Supprimer l'image

+ {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }} + {% endif %} + + +
+{% endmacro %} + +{% macro render_logos(dept_form) %} + + {% for logo_entry in dept_form.logos.entries %} + {% set logo_form = logo_entry.form %} + {{ render_logo(dept_form, logo_form) }} + {% else %} +

+

Aucun logo défini en propre à ce département

+

+ {% endfor %} +
+{% endmacro %} + +{% block app_content %} + + + + + + {{ form.hidden_tag() }} + + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 1eee13d4..888d9591 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -19,104 +19,46 @@ {% endmacro %} -{% macro render_add_logo(add_logo_form) %} -
-

Ajouter un logo

- {{ add_logo_form.hidden_tag() }} - {{ render_field(add_logo_form.name) }} - {{ render_field(add_logo_form.upload) }} - {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }} -
-{% endmacro %} - -{% macro render_logo(dept_form, logo_form) %} -
- {{ logo_form.hidden_tag() }} - {% if logo_form.titre %} - - -

{{ logo_form.titre }}

-
{{ logo_form.description or "" }}
- - - {% else %} - - -

Logo personalisé: {{ logo_form.logo_id.data }}

- {{ logo_form.description or "" }} - - - {% endif %} - - -
- pas de logo chargé
- -

{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})

- Taille: {{ logo_form.logo.size }} px - {% if logo_form.logo.mm %}   /   {{ logo_form.logo.mm }} mm {% endif %}
- Aspect ratio: {{ logo_form.logo.aspect_ratio }}
- Usage: {{ logo_form.logo.get_usage() }} - -

Modifier l'image

- {{ render_field(logo_form.upload, False, onchange="submit_form()") }} - {% if logo_form.can_delete %} -

Supprimer l'image

- {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }} - {% endif %} - - -
-{% endmacro %} - -{% macro render_logos(dept_form) %} - - {% for logo_entry in dept_form.logos.entries %} - {% set logo_form = logo_entry.form %} - {{ render_logo(dept_form, logo_form) }} - {% else %} -

Aucun logo défini en propre à ce département

- {% endfor %} -
-{% endmacro %} - {% block app_content %} - - -
{{ form.hidden_tag() }} -
-{% endblock %} \ No newline at end of file + +{% endblock %} + +{% block scripts %} +{{ super() }} + + +{% endblock %} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index ea2f2a41..234d688c 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -45,7 +45,7 @@ from werkzeug.exceptions import BadRequest, NotFound from app import db from app.auth.models import User -from app.forms.main import config_forms +from app.forms.main import config_logos, config_main from app.forms.main.create_dept import CreateDeptForm from app.forms.main.config_apo import CodesDecisionsForm from app import models @@ -250,10 +250,26 @@ def about(scodoc_dept=None): @bp.route("/ScoDoc/configuration", methods=["GET", "POST"]) @admin_required def configuration(): - auth_name = str(current_user) + "Page de configuration globale" if not current_user.is_administrator(): - raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) - return config_forms.configuration() + raise AccessDenied("invalid user (%s) must be SuperAdmin" % current_user) + return config_main.configuration() + + +@bp.route("/ScoDoc/get_bonus_description/", methods=["GET"]) +def get_bonus_description(bonus_name: str): + "description text/html du bonus" + bonus_class = ScoDocSiteConfig.get_bonus_sport_class_from_name(bonus_name) + return bonus_class.__doc__ + + +@bp.route("/ScoDoc/configure_logos", methods=["GET", "POST"]) +@admin_required +def configure_logos(): + "Page de configuration des logos (globale)" + if not current_user.is_administrator(): + raise AccessDenied("invalid user (%s) must be SuperAdmin" % current_user) + return config_logos.config_logos() SMALL_SIZE = (200, 200) diff --git a/scodoc.py b/scodoc.py index 451ebee2..a18251fd 100755 --- a/scodoc.py +++ b/scodoc.py @@ -21,7 +21,6 @@ from app import clear_scodoc_cache from app import models from app.auth.models import User, Role, UserRole -from app.models import ScoPreference from app.scodoc.sco_logos import make_logo_local from app.models import Formation, UniteEns, Module from app.models import FormSemestre, FormSemestreInscription From 4c5c20ce7e4ef3d86c704c1e8225d3146eaccb29 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 00:11:04 +0100 Subject: [PATCH 27/70] comptes absences dans bul json BUT --- app/but/bulletin_but.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 1c45d6e0..79d3b776 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -16,6 +16,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod from app.models import ModuleImpl from app.scodoc import sco_utils as scu from app.scodoc.sco_cache import ResultatsSemestreBUTCache +from app.scodoc import sco_abs from app.scodoc import sco_bulletins_json from app.scodoc import sco_preferences from app.scodoc.sco_utils import jsnan, fmt_note @@ -244,6 +245,8 @@ class ResultatsSemestreBUT: } if not published: return d + + nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) semestre_infos = { "etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo], "date_debut": formsemestre.date_debut.isoformat(), @@ -252,9 +255,9 @@ class ResultatsSemestreBUT: "inscription": "TODO-MM-JJ", # XXX TODO "numero": formsemestre.semestre_id, "groupes": [], # XXX TODO - "absences": { # XXX TODO - "injustifie": 1, - "total": 33, + "absences": { + "injustifie": nbabsjust, + "total": nbabs, }, } semestre_infos.update( From 1488823c5d16b83c73d3d823f0ea17d29275ff91 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 08:21:13 +0100 Subject: [PATCH 28/70] Rename aux.py for Windows --- app/comp/{aux.py => aux_stats.py} | 0 app/comp/res_common.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/comp/{aux.py => aux_stats.py} (100%) diff --git a/app/comp/aux.py b/app/comp/aux_stats.py similarity index 100% rename from app/comp/aux.py rename to app/comp/aux_stats.py diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 9356bf89..e2e5ab16 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -8,7 +8,7 @@ from collections import defaultdict, Counter from functools import cached_property import numpy as np import pandas as pd -from app.comp.aux import StatsMoyenne +from app.comp.aux_stats import StatsMoyenne from app.comp.moy_mod import ModuleImplResults from app.models import FormSemestre, Identite, ModuleImpl from app.models.ues import UniteEns From d0daecdb7f6eaf4daa0d689434c4c2f2ca2ba1b8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 08:21:33 +0100 Subject: [PATCH 29/70] fix import after merge --- app/but/bulletin_but.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 95e18156..c81f3678 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -11,11 +11,6 @@ import datetime from flask import url_for, g from app.scodoc import sco_utils as scu -<<<<<<< HEAD -======= -from app.scodoc.sco_cache import ResultatsSemestreBUTCache -from app.scodoc import sco_abs ->>>>>>> 4c5c20ce7e4ef3d86c704c1e8225d3146eaccb29 from app.scodoc import sco_bulletins_json from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT From 4ae138c496781b86a8896ef01bddc8e4976e72a2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 11:51:13 +0100 Subject: [PATCH 30/70] Ajout description bonus sur bulletin BUT json --- app/but/bulletin_but.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index c81f3678..5df15806 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -59,9 +59,9 @@ class BulletinBUT(ResultatsSemestreBUT): "competence": None, # XXX TODO lien avec référentiel "moyenne": None, # Le bonus sport appliqué sur cette UE - "bonus": self.bonus_ues[ue.id][etud.id] + "bonus": fmt_note(self.bonus_ues[ue.id][etud.id]) if self.bonus_ues is not None and ue.id in self.bonus_ues - else 0.0, + else fmt_note(0.0), "malus": None, # XXX TODO voir ce qui est ici "capitalise": None, # "AAAA-MM-JJ" TODO "ressources": self.etud_ue_mod_results(etud, ue, self.ressources), @@ -74,6 +74,16 @@ class BulletinBUT(ResultatsSemestreBUT): "max": fmt_note(self.etud_moy_ue[ue.id].max()), "moy": fmt_note(self.etud_moy_ue[ue.id].mean()), } + else: + # ceci suppose que l'on a une seule UE bonus, + # en tous cas elles auront la même description + d["bonus_description"] = self.etud_bonus_description(etud.id) + modimpls_spo = [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if modimpl.module.ue.type == UE_SPORT + ] + d["modules"] = self.etud_mods_results(etud, modimpls_spo) return d def etud_mods_results(self, etud, modimpls) -> dict: @@ -152,6 +162,27 @@ class BulletinBUT(ResultatsSemestreBUT): } return d + def etud_bonus_description(self, etudid): + """description du bonus affichée dans la section "UE bonus".""" + if self.bonus_ues is None or self.bonus_ues.shape[1] == 0: + return "" + import random + + bonus_vect = self.bonus_ues.loc[etudid] + [random.random() for i in range(3)] + if bonus_vect.nunique() > 1: + # détail UE par UE + details = [ + f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}" + for ue in self.ues + if ue.id in self.bonus_ues and bonus_vect[ue.id] > 0.0 + ] + if details: + return "Bonus de " + ", ".join(details) + else: + return "" # aucun bonus + else: + return f"Bonus de {fmt_note(bonus_vect.iloc[0])}" + def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict: """Le bulletin de l'étudiant dans ce semestre. Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai From a47faa0115e74579109ab09e9f971f827fbfe764 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Wed, 26 Jan 2022 14:27:55 +0100 Subject: [PATCH 31/70] Fix PE generation --- app/pe/pe_tools.py | 2 +- app/pe/pe_view.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index a495f965..2c9dd230 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -206,7 +206,7 @@ def add_pe_stuff_to_zip(zipfile, ziproot): for name in logos_names: logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) if logo is not None: - add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename) + add_local_file_to_zip(zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename) # ---------------------------------------------------------------------------------------- diff --git a/app/pe/pe_view.py b/app/pe/pe_view.py index 43a00ebe..5a98d375 100644 --- a/app/pe/pe_view.py +++ b/app/pe/pe_view.py @@ -97,7 +97,7 @@ def pe_view_sem_recap( template_latex = "" # template fourni via le formulaire Web if avis_tmpl_file: - template_latex = avis_tmpl_file.read() + template_latex = avis_tmpl_file.read().decode('utf-8') template_latex = template_latex else: # template indiqué dans préférences ScoDoc ? @@ -114,7 +114,7 @@ def pe_view_sem_recap( footer_latex = "" # template fourni via le formulaire Web if footer_tmpl_file: - footer_latex = footer_tmpl_file.read() + footer_latex = footer_tmpl_file.read().decode('utf-8') footer_latex = footer_latex else: footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference( From 8081df686bc89177d8f4e0e447cbd4c98c8b49c4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 14:46:14 +0100 Subject: [PATCH 32/70] Essai bonus Tours --- app/comp/bonus_spo.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index bba0cd47..ee54262d 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -244,7 +244,7 @@ class BonusDirect(BonusSportSimples): """ name = "bonus_direct" - seuil_moy_gen = 0.0 # seuls le spoints au dessus du seuil sont comptés + seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés proportion_point = 1.0 @@ -273,7 +273,22 @@ class BonusColmar(BonusSportSimples): bonus_moy_gen_limit = 0.5 -class BonusVilleAvray: +class BonusTours(BonusSportSimples): + """Calcul bonus sport & culture IUT Tours. + + Les notes des UE bonus sont sommées et ajoutées aux moyennes: soit à la + moyenne générale, soit pour le BUT à chaque moyenne d'UE. + Le bonus est limité à 1 point. + """ + + name = "bonus_tours" + bonus_moy_gen_limit = 1.0 + seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés + proportion_point = 1.0 + + +# ---- Un peu moins simples (mais pas trop compliqué) +class BonusVilleAvray(BonusSport): """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray Les étudiants de l'IUT peuvent suivre des enseignements optionnels From 536ee1781bfc3fa7c14798c1d33f5ab1cf686113 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 18:35:42 +0100 Subject: [PATCH 33/70] Correction bonus IUT Tours --- app/comp/bonus_spo.py | 12 +++++++----- app/scodoc/bonus_sport.py | 6 ++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index ee54262d..15d4726b 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -276,15 +276,17 @@ class BonusColmar(BonusSportSimples): class BonusTours(BonusSportSimples): """Calcul bonus sport & culture IUT Tours. - Les notes des UE bonus sont sommées et ajoutées aux moyennes: soit à la - moyenne générale, soit pour le BUT à chaque moyenne d'UE. - Le bonus est limité à 1 point. + Les notes des UE bonus (ramenées sur 20) sont sommées + et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale, + soit pour le BUT à chaque moyenne d'UE. + + Le bonus total est limité à 1 point. """ name = "bonus_tours" - bonus_moy_gen_limit = 1.0 + bonus_moy_gen_limit = 1.0 # seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés - proportion_point = 1.0 + proportion_point = 1.0 / 40.0 # ---- Un peu moins simples (mais pas trop compliqué) diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 5351ecb3..1e754f82 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -28,10 +28,12 @@ from operator import mul import pprint -""" +""" ANCIENS BONUS SPORT pour ScoDoc < 9.2 NON UTILISES A PARTIR DE 9.2 (voir comp/bonus_spo.py) + La fonction bonus_sport reçoit: - - notes_sport: la liste des notes des modules de sport et culture (une note par module de l'UE de type sport/culture); + - notes_sport: la liste des notes des modules de sport et culture (une note par module + de l'UE de type sport/culture, toujours dans remise sur 20); - coefs: un coef (float) pondérant chaque note (la plupart des bonus les ignorent); - infos: dictionnaire avec des données pouvant être utilisées pour les calculs. Ces données dépendent du type de formation. From 4222ea8160ae5e02d31367e244f47532d40b120f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 18:42:53 +0100 Subject: [PATCH 34/70] Bonus Lille --- app/comp/bonus_spo.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 15d4726b..15472b59 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -12,6 +12,7 @@ Les classes de Bonus fournissent deux méthodes: """ +import datetime import numpy as np import pandas as pd @@ -244,11 +245,11 @@ class BonusDirect(BonusSportSimples): """ name = "bonus_direct" - seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés + seuil_moy_gen = 0.0 # tous les points sont comptés proportion_point = 1.0 -class BonusIUTStDenis(BonusIUTV): +class BonusStDenis(BonusIUTV): """Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.""" name = "bonus_iut_stdenis" @@ -273,7 +274,7 @@ class BonusColmar(BonusSportSimples): bonus_moy_gen_limit = 0.5 -class BonusTours(BonusSportSimples): +class BonusTours(BonusDirect): """Calcul bonus sport & culture IUT Tours. Les notes des UE bonus (ramenées sur 20) sont sommées @@ -290,6 +291,35 @@ class BonusTours(BonusSportSimples): # ---- Un peu moins simples (mais pas trop compliqué) + + +# Bonus simple, mais avec chagement de paramètres en 2010 ! +class BonusLille(BonusSportSimples): + """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Lille 1 (sports, etc) non rattachés à une unité d'enseignement. + + Les points au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés + s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + name = "bonus_lille" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # La date du semestre ? + if self.formsemestre.date_debut > datetime.date(2010, 8, 1): + self.proportion_point = 0.04 + else: + self.proportion_point = 0.02 + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) + + class BonusVilleAvray(BonusSport): """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray From 3c36acd19483ab0b9d8db311cd28211ec28557d8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 20:38:27 +0100 Subject: [PATCH 35/70] =?UTF-8?q?Bonus=20grenoble=202020=20(=C3=A0=20compl?= =?UTF-8?q?=C3=A9ter=20pour=20l'ancien)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/bonus_spo.py | 67 +++++++++++++++++++++++++++++++++++++++-- app/comp/res_but.py | 2 ++ app/comp/res_classic.py | 2 ++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 15472b59..7427089d 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -49,6 +49,10 @@ class BonusSport: - modimpl_coefs: les coefs des modules En classique: 1d ndarray de float (modimpl) En APC: 2d ndarray de float, (modimpl x UE) <= attention à transposer + - etud_moy_gen: Series, index etudid, valeur float (moyenne générale avant bonus) + - etud_moy_ue: DataFrame columns UE (sans sport), rows etudid (moyennes avant bonus) + + etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs). """ # Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen: @@ -67,9 +71,13 @@ class BonusSport: ues: list, modimpl_inscr_df: pd.DataFrame, modimpl_coefs: np.array, + etud_moy_gen, + etud_moy_ue, ): self.formsemestre = formsemestre self.ues = ues + self.etud_moy_gen = etud_moy_gen + self.etud_moy_ue = etud_moy_ue self.etuds_idx = modimpl_inscr_df.index # les étudiants inscrits au semestre self.bonus_ues: pd.DataFrame = None # virtual self.bonus_moy_gen: pd.Series = None # virtual @@ -293,7 +301,7 @@ class BonusTours(BonusDirect): # ---- Un peu moins simples (mais pas trop compliqué) -# Bonus simple, mais avec chagement de paramètres en 2010 ! +# Bonus simple, mais avec changement de paramètres en 2010 ! class BonusLille(BonusSportSimples): """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq @@ -320,8 +328,63 @@ class BonusLille(BonusSportSimples): ) +def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None): + """Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017) + + La note de sport de nos étudiants va de 0 à 5 points. + Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale. + Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%. + + Calcul ici du bonus sur moyenne générale + """ + # les coefs sont ignorés + # notes de 0 à 5 + points = sum([x for x in notes_sport]) + factor = (points / 4.0) / 100.0 + bonus = infos["moy"] * factor + + return bonus + + +class BonusGrenobleIUT1(BonusSport): + """ + La note de sport est sur 20, et on calcule une bonification (en %) + qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant + la formule : bonification (en %) = (note-10)*0,5. + + Bonification qui ne s'applique que si la note est >10. + + (Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif) + + """ + + name = "bonus_grenoble_iut1_2020" + # C'est un bonus "multiplicatif": on l'exprime en additif, + # sur chaque moyenne d'UE m_0 + # m_1 = a . m_0 + # m_1 = m_0 + bonus + # bonus = m_0 (a - 1) + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # Calcule moyenne pondérée des notes de sport: + notes = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + notes = np.nan_to_num(notes, copy=False) + a = (notes - 10.0) * 0.005 + a[a <= 0] = 1.0 # note < 10, pas de bonus + if self.formsemestre.formation.is_apc(): + # ne s'applique qu'aux moyennes d'UE + b = self.etud_moy_ue * (a - 1) + self.bonus_ues = b # DataFrame + else: + # ne s'applique qu'à la moyenne générale + b = self.etud_moy_gen * (a - 1) + self.bonus_moy_gen = b + + class BonusVilleAvray(BonusSport): - """Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray + """Bonus modules optionels (sport, culture), règle IUT Ville d'Avray. Les étudiants de l'IUT peuvent suivre des enseignements optionnels de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement. diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 2ae263d5..e1fcdc27 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -73,6 +73,8 @@ class ResultatsSemestreBUT(NotesTableCompat): self.ues, self.modimpl_inscr_df, self.modimpl_coefs_df.transpose(), + self.etud_moy_gen, + self.etud_moy_ue, ) self.bonus_ues = bonus.get_bonus_ues() if self.bonus_ues is not None: diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 3c742867..65c83e64 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -80,6 +80,8 @@ class ResultatsSemestreClassic(NotesTableCompat): self.ues, self.modimpl_inscr_df, self.modimpl_coefs, + self.etud_moy_gen, + self.etud_moy_ue, ) self.bonus_ues = bonus.get_bonus_ues() if self.bonus_ues is not None: From d146d5f554e38cbd51c3ecf8d7f693095072165e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 22:52:56 +0100 Subject: [PATCH 36/70] =?UTF-8?q?Traitement=20erreur=20config=20bonus=20+?= =?UTF-8?q?=20compl=C3=A9t=C3=A9=20bonus=20Grenoble?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- app/comp/bonus_spo.py | 38 +++++++++++++++++++++++++------------- app/models/config.py | 8 +++++--- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 5827703c..209a2a01 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,12 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes). -### État actuel (4 dec 21) +### État actuel (26 jan 22) - - 9.0 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: - - ancien module "Entreprises" (obsolète) + - 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: + - ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT. - - 9.1 (branche "PNBUT") est la version de développement. + - 9.2 (branche refactor_nt) est la version de développement. ### Lignes de commandes diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 7427089d..0e63adaf 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -338,7 +338,7 @@ def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None): Calcul ici du bonus sur moyenne générale """ # les coefs sont ignorés - # notes de 0 à 5 + # notes de 0 à 5/20 points = sum([x for x in notes_sport]) factor = (points / 4.0) / 100.0 bonus = infos["moy"] * factor @@ -347,7 +347,9 @@ def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None): class BonusGrenobleIUT1(BonusSport): - """ + """Bonus IUT1 de Grenoble + + À compter de sept. 2021: La note de sport est sur 20, et on calcule une bonification (en %) qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant la formule : bonification (en %) = (note-10)*0,5. @@ -356,11 +358,16 @@ class BonusGrenobleIUT1(BonusSport): (Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif) + Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20). + Chaque point correspondait à 0.25% d'augmentation de la moyenne + générale. + Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%. """ - name = "bonus_grenoble_iut1_2020" + name = "bonus_iut1grenoble_2017" # C'est un bonus "multiplicatif": on l'exprime en additif, # sur chaque moyenne d'UE m_0 + # Augmenter de 5% correspond à multiplier par a=1.05 # m_1 = a . m_0 # m_1 = m_0 + bonus # bonus = m_0 (a - 1) @@ -371,16 +378,21 @@ class BonusGrenobleIUT1(BonusSport): sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) notes = np.nan_to_num(notes, copy=False) - a = (notes - 10.0) * 0.005 - a[a <= 0] = 1.0 # note < 10, pas de bonus - if self.formsemestre.formation.is_apc(): - # ne s'applique qu'aux moyennes d'UE - b = self.etud_moy_ue * (a - 1) - self.bonus_ues = b # DataFrame - else: - # ne s'applique qu'à la moyenne générale - b = self.etud_moy_gen * (a - 1) - self.bonus_moy_gen = b + + if self.formsemestre.date_debut > datetime.date(2021, 7, 15): + factor = (notes - 10.0) * 0.005 # 5% si note=20 + factor[factor <= 0] = 0.0 # note < 10, pas de bonus + else: # anciens semestres + factor = notes / 400.0 + factor[factor <= 0] = 0.0 # facteur 1 si bonus nul + + # S'applique qu'aux moyennes d'UE + bonus = self.etud_moy_ue * factor + self.bonus_ues = bonus # DataFrame + + if not self.formsemestre.formation.is_apc(): + # s'applique à la moyenne générale + self.bonus_moy_gen = bonus class BonusVilleAvray(BonusSport): diff --git a/app/models/config.py b/app/models/config.py index c69b5ae6..da98998c 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -3,6 +3,7 @@ """Model : site config WORK IN PROGRESS #WIP """ +from flask import flash from app import db, log from app.comp import bonus_spo from app.scodoc.sco_exceptions import ScoValueError @@ -120,7 +121,8 @@ class ScoDocSiteConfig(db.Model): """returns bonus class with specified name. If name not specified, return the configured function. None if no bonus function configured. - Raises ScoValueError if class_name not found in module bonus_sport. + If class_name not found in module bonus_sport, returns None + and flash a warning. """ if class_name is None: c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() @@ -131,9 +133,9 @@ class ScoDocSiteConfig(db.Model): return None klass = bonus_spo.get_bonus_class_dict().get(class_name) if klass is None: - raise ScoValueError( + flash( f"""Fonction de calcul bonus sport inexistante: {class_name}. - (contacter votre administrateur local).""" + Changez là ou contactez votre administrateur local.""" ) return klass From 8473270ee632db0fae37a854ca0b3b3782a910de Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 22:59:53 +0100 Subject: [PATCH 37/70] clip bonus --- app/comp/res_but.py | 9 ++++++--- app/comp/res_classic.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index e1fcdc27..e7c6d4c4 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -61,9 +61,7 @@ class ResultatsSemestreBUT(NotesTableCompat): self.etud_coef_ue_df = pd.DataFrame( 1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns ) - self.etud_moy_gen = moy_sem.compute_sem_moys_apc( - self.etud_moy_ue, modimpl_coefs_no_bonus_df - ) + # --- Bonus Sport & Culture bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: @@ -79,7 +77,12 @@ class ResultatsSemestreBUT(NotesTableCompat): self.bonus_ues = bonus.get_bonus_ues() if self.bonus_ues is not None: self.etud_moy_ue += self.bonus_ues # somme les dataframes + self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) + # Moyenne générale indicative: + self.etud_moy_gen = moy_sem.compute_sem_moys_apc( + self.etud_moy_ue, modimpl_coefs_no_bonus_df + ) self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 65c83e64..8d52d0c3 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -86,9 +86,11 @@ class ResultatsSemestreClassic(NotesTableCompat): self.bonus_ues = bonus.get_bonus_ues() if self.bonus_ues is not None: self.etud_moy_ue += self.bonus_ues # somme les dataframes + self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) bonus_mg = bonus.get_bonus_moy_gen() if bonus_mg is not None: self.etud_moy_gen += bonus_mg + self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True) self.bonus = ( bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins ) From b8abc846c6ba771efd59bd7f53bbab65f19dbd98 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 23:46:46 +0100 Subject: [PATCH 38/70] bonus: refactoring + Le Havre, Nantes, Roanne --- app/comp/bonus_spo.py | 143 +++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 50 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 0e63adaf..d442647a 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -16,12 +16,7 @@ import datetime import numpy as np import pandas as pd -from app import db -from app import models -from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef -from app.comp import moy_mod from app.models.formsemestre import FormSemestre -from app.scodoc import bonus_sport from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -57,6 +52,8 @@ class BonusSport: # Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen: apc_apply_bonus_mg_to_ues = True + # Si True, reporte toujours le bonus moy gen sur les UE (même en formations classiques) + apply_bonus_mg_to_ues = False # Attributs virtuels: seuil_moy_gen = None proportion_point = None @@ -167,10 +164,9 @@ class BonusSport: """Les bonus à appliquer aux UE Résultat: DataFrame de float, index etudid, columns: ue.id """ - if ( - self.formsemestre.formation.is_apc() - and self.apc_apply_bonus_mg_to_ues - and self.bonus_ues is None + if self.bonus_ues is None and ( + (self.apc_apply_bonus_mg_to_ues and self.formsemestre.formation.is_apc()) + or self.apply_bonus_mg_to_ues ): # reporte uniformément le bonus moyenne générale sur les UEs # (assure la compatibilité de la plupart des anciens bonus avec le BUT) @@ -189,10 +185,10 @@ class BonusSport: return self.bonus_moy_gen -class BonusSportSimples(BonusSport): - """Les bonus sport simples calcule un bonus à partir des notes moyennes de modules - de l'UE sport, et ce bonus est soit appliqué sur la moyenne générale (formations classiques), - soit réparti sur les UE (formations APC). +class BonusSportAdditif(BonusSport): + """Bonus sport simples calcule un bonus à partir des notes moyennes de modules + de l'UE sport, et ce bonus est soit ajouté à la moyenne générale (formations classiques), + soit ajouté à chaque UE (formations APC). Le bonus est par défaut calculé comme: Les points au-dessus du seuil (par défaut) 10 sur 20 obtenus dans chacun des @@ -231,7 +227,7 @@ class BonusSportSimples(BonusSport): # bonus_ue = np.stack([modimpl_coefs_spo.T] * nb_ues) -class BonusIUTV(BonusSportSimples): +class BonusIUTV(BonusSportAdditif): """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse Les étudiants de l'IUT peuvent suivre des enseignements optionnels @@ -246,7 +242,7 @@ class BonusIUTV(BonusSportSimples): pass # oui, c'ets le bonus par défaut -class BonusDirect(BonusSportSimples): +class BonusDirect(BonusSportAdditif): """Bonus direct: les points sont directement ajoutés à la moyenne générale. Les coefficients sont ignorés: tous les points de bonus sont sommés. (rappel: la note est ramenée sur 20 avant application). @@ -264,7 +260,7 @@ class BonusStDenis(BonusIUTV): bonus_moy_gen_limit = 0.5 -class BonusColmar(BonusSportSimples): +class BonusColmar(BonusSportAdditif): """Calcul bonus modules optionels (sport, culture), règle IUT Colmar. Les étudiants de l'IUT peuvent suivre des enseignements optionnels @@ -298,11 +294,8 @@ class BonusTours(BonusDirect): proportion_point = 1.0 / 40.0 -# ---- Un peu moins simples (mais pas trop compliqué) - - # Bonus simple, mais avec changement de paramètres en 2010 ! -class BonusLille(BonusSportSimples): +class BonusLille(BonusSportAdditif): """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq Les étudiants de l'IUT peuvent suivre des enseignements optionnels @@ -328,25 +321,39 @@ class BonusLille(BonusSportSimples): ) -def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None): - """Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017) +class BonusSportMultiplicatif(BonusSport): + """Bonus sport qui multiplie les moyennes d'UE par un facteur""" - La note de sport de nos étudiants va de 0 à 5 points. - Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale. - Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%. + seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés + amplitude = 0.005 # multiplie les points au dessus du seuil - Calcul ici du bonus sur moyenne générale - """ - # les coefs sont ignorés - # notes de 0 à 5/20 - points = sum([x for x in notes_sport]) - factor = (points / 4.0) / 100.0 - bonus = infos["moy"] * factor + # C'est un bonus "multiplicatif": on l'exprime en additif, + # sur chaque moyenne d'UE m_0 + # Augmenter de 5% correspond à multiplier par a=1.05 + # m_1 = a . m_0 + # m_1 = m_0 + bonus + # bonus = m_0 (a - 1) + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # Calcule moyenne pondérée des notes de sport: + notes = np.sum( + sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + notes = np.nan_to_num(notes, copy=False) - return bonus + factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20 + factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus + + # S'applique qu'aux moyennes d'UE + bonus = self.etud_moy_ue * factor + self.bonus_ues = bonus # DataFrame + + if not self.formsemestre.formation.is_apc(): + # s'applique à la moyenne générale + self.bonus_moy_gen = bonus -class BonusGrenobleIUT1(BonusSport): +class BonusGrenobleIUT1(BonusSportMultiplicatif): """Bonus IUT1 de Grenoble À compter de sept. 2021: @@ -372,27 +379,63 @@ class BonusGrenobleIUT1(BonusSport): # m_1 = m_0 + bonus # bonus = m_0 (a - 1) def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): - """calcul du bonus""" - # Calcule moyenne pondérée des notes de sport: - notes = np.sum( - sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) - notes = np.nan_to_num(notes, copy=False) + """calcul du bonus, avec réglage différent suivant la date""" if self.formsemestre.date_debut > datetime.date(2021, 7, 15): - factor = (notes - 10.0) * 0.005 # 5% si note=20 - factor[factor <= 0] = 0.0 # note < 10, pas de bonus + self.seuil_moy_gen = 10.0 + self.amplitude = 0.005 else: # anciens semestres - factor = notes / 400.0 - factor[factor <= 0] = 0.0 # facteur 1 si bonus nul + self.seuil_moy_gen = 0.0 + self.amplitude = 1 / 400.0 - # S'applique qu'aux moyennes d'UE - bonus = self.etud_moy_ue * factor - self.bonus_ues = bonus # DataFrame + super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) - if not self.formsemestre.formation.is_apc(): - # s'applique à la moyenne générale - self.bonus_moy_gen = bonus + +class BonusLeHavre(BonusSportMultiplicatif): + """Bonus sport IUT du Havre sur moyenne générale et UE + + Les points des modules bonus au dessus de 10/20 sont ajoutés, + et les moyennes d'UE augmentées de 5% de ces points. + """ + + name = "bonus_iutlh" + seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés + amplitude = 0.005 # multiplie les points au dessus du seuil + + +class BonusNantes(BonusSportAdditif): + """IUT de Nantes (Septembre 2018) + + Nous avons différents types de bonification + (sport, culture, engagement citoyen). + + Nous ajoutons aux moyennes une bonification de 0,2 pour chaque item + la bonification totale ne doit pas excéder les 0,5 point. + Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications. + + Dans ScoDoc: on a déclarera une UE "sport&culture" dans laquelle on aura des modules + pour chaque activité (Sport, Associations, ...) + avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la + valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale) + """ + + name = "bonus_nantes" + seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés + proportion_point = 1 # multiplie les points au dessus du seuil + bonus_moy_gen_limit = 0.5 # plafonnement à 0.5 points + + +class BonusRoanne(BonusSportAdditif): + """IUT de Roanne. + + Le bonus est compris entre 0 et 0.35 point + et est toujours appliqué aux UEs. + """ + + name = "bonus_iutr" + seuil_moy_gen = 0.0 + bonus_moy_gen_limit = 0.35 # plafonnement à 0.35 points + apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP class BonusVilleAvray(BonusSport): From a95338743739deb8fcc0dfd8264946650d05e3ec Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 26 Jan 2022 23:51:46 +0100 Subject: [PATCH 39/70] =?UTF-8?q?r=C3=A9active=20avis=20PE=20pour=20essais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_status.py | 4 ++-- sco_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 5c318989..cf72ed18 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -105,10 +105,10 @@ def _build_menu_stats(formsemestre_id): "enabled": True, }, { - "title": "Documents Avis Poursuite Etudes", + "title": "Documents Avis Poursuite Etudes (xp)", "endpoint": "notes.pe_view_sem_recap", "args": {"formsemestre_id": formsemestre_id}, - "enabled": current_app.config["TESTING"] or current_app.config["DEBUG"], + "enabled": True, # current_app.config["TESTING"] or current_app.config["DEBUG"], }, { "title": 'Table "débouchés"', diff --git a/sco_version.py b/sco_version.py index 9540dbe5..8ef59e41 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.33" +SCOVERSION = "9.1.34" SCONAME = "ScoDoc" From 715d7aa9ee89ef02297167a79fc53c247ec4cca5 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 00:18:50 +0100 Subject: [PATCH 40/70] 9.1.35 --- app/scodoc/sco_edit_ue.py | 12 ++++++++++-- sco_version.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index c9a2da17..1de9969e 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -33,13 +33,15 @@ from flask import url_for, render_template from flask import g, request from flask_login import current_user +from app import db +from app import log from app.models import APO_CODE_STR_LEN from app.models import Formation, UniteEns, ModuleImpl, Module +from app.models.formations import Matiere import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType -from app import log -from app.scodoc.TrivialFormulator import TrivialFormulator, TF +from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.gen_tables import GenTable from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( @@ -533,6 +535,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list # pour faciliter la transition des anciens programmes non APC for ue in ues_obj: ue.guess_semestre_idx() + # vérifie qu'on a bien au moins une matière dans chaque UE + for ue in ues_obj: + if ue.matieres.count() < 1: + mat = Matiere(ue_id=ue.id) + db.session.add(mat) + db.session.commit() ues = [ue.to_dict() for ue in ues_obj] ues_externes = [ue.to_dict() for ue in ues_externes_obj] diff --git a/sco_version.py b/sco_version.py index 8ef59e41..00415b08 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.34" +SCOVERSION = "9.1.35" SCONAME = "ScoDoc" From 70a00cd1b54b21421ae0d1f7988d9916cc5a2596 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 10:22:42 +0100 Subject: [PATCH 41/70] Lien sur message erreur saisie incorrecte eval --- app/scodoc/sco_evaluation_db.py | 3 ++- app/scodoc/sco_exceptions.py | 2 +- sco_version.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 17d47915..6586ddd0 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -185,7 +185,8 @@ def _check_evaluation_args(args): if (jour > date_fin) or (jour < date_debut): raise ScoValueError( "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" - % (d, m, y) + % (d, m, y), + dest_url="javascript:history.back();", ) heure_debut = args.get("heure_debut", None) args["heure_debut"] = heure_debut diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 5f64f57b..112658e6 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -40,7 +40,7 @@ class InvalidNoteValue(ScoException): pass -# Exception qui stoque dest_url, utilisee dans Zope standard_error_message +# Exception qui stoque dest_url class ScoValueError(ScoException): def __init__(self, msg, dest_url=None): super().__init__(msg) diff --git a/sco_version.py b/sco_version.py index 00415b08..c3e72520 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.35" +SCOVERSION = "9.1.36" SCONAME = "ScoDoc" From f89e74c53ccf42507617be84775a3bc91819ed50 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 11:44:58 +0100 Subject: [PATCH 42/70] =?UTF-8?q?Am=C3=A9liore=20=C3=A9dition=20module=20(?= =?UTF-8?q?rattachements=20UE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_edit_module.py | 58 +++++++++++++++++++---------------- scodoc.py | 3 +- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index b575123e..8359d12b 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -476,31 +476,21 @@ def module_edit(module_id=None): parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) is_apc = parcours.APC_SAE # BUT in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls + matieres = Matiere.query.filter( + Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation_id + ).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero) if in_use: - # matières du même semestre seulement - ues_matieres = ndb.SimpleDictFetch( - """SELECT ue.acronyme, mat.*, mat.id AS matiere_id - FROM notes_matieres mat, notes_ue ue - WHERE mat.ue_id = ue.id - AND ue.formation_id = %(formation_id)s - AND ue.semestre_idx = %(semestre_idx)s - ORDER BY ue.numero, mat.numero - """, - {"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx}, - ) + # restreint aux matières du même semestre + matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx) + + if is_apc: + mat_names = [ + "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres + ] else: - # matières de la formation - ues_matieres = ndb.SimpleDictFetch( - """SELECT ue.acronyme, mat.*, mat.id AS matiere_id - FROM notes_matieres mat, notes_ue ue - WHERE mat.ue_id = ue.id - AND ue.formation_id = %(formation_id)s - ORDER BY ue.numero, mat.numero - """, - {"formation_id": formation_id}, - ) - mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres] - ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres] + mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] + ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] + module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) semestres_indices = list(range(1, parcours.NB_SEM + 1)) @@ -564,11 +554,17 @@ def module_edit(module_id=None): ), ( "heures_cours", - {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"}, + { + "title": "Heures CM :", + "size": 4, + "type": "float", + "explanation": "nombre d'heures de cours", + }, ), ( "heures_td", { + "title": "Heures TD :", "size": 4, "type": "float", "explanation": "nombre d'heures de Travaux Dirigés", @@ -577,6 +573,7 @@ def module_edit(module_id=None): ( "heures_tp", { + "title": "Heures TP :", "size": 4, "type": "float", "explanation": "nombre d'heures de Travaux Pratiques", @@ -596,9 +593,9 @@ def module_edit(module_id=None): "ue_coefs", { "readonly": True, - "title": "Coefficients vers les UE", + "title": "Coefficients vers les UE ", "default": coefs_descr_txt, - "explanation": "passer par la page d'édition de la formation pour modifier les coefficients", + "explanation": "
(passer par la page d'édition de la formation pour modifier les coefficients)", }, ) ] @@ -624,7 +621,14 @@ def module_edit(module_id=None): { "input_type": "menu", "title": "Rattachement :" if is_apc else "Matière :", - "explanation": "UE de rattachement, utilisée pour la présentation" + "explanation": ( + "UE de rattachement" + + ( + " module utilisé, ne peut pas être changé de semestre" + if in_use + else "" + ) + ) if is_apc else "un module appartient à une seule matière.", "labels": mat_names, diff --git a/scodoc.py b/scodoc.py index a18251fd..28dfe7cf 100755 --- a/scodoc.py +++ b/scodoc.py @@ -22,7 +22,7 @@ from app import models from app.auth.models import User, Role, UserRole from app.scodoc.sco_logos import make_logo_local -from app.models import Formation, UniteEns, Module +from app.models import Formation, UniteEns, Matiere, Module from app.models import FormSemestre, FormSemestreInscription from app.models import ModuleImpl, ModuleImplInscription from app.models import Identite @@ -62,6 +62,7 @@ def make_shell_context(): "logout_user": logout_user, "mapp": mapp, "models": models, + "Matiere": Matiere, "Module": Module, "ModuleImpl": ModuleImpl, "ModuleImplInscription": ModuleImplInscription, From 8aba6d6632895c2fbe5b3960e4c3b54a6c327d88 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 11:53:16 +0100 Subject: [PATCH 43/70] Fix: do_etud_inscrit_ue --- app/scodoc/sco_moduleimpl_inscriptions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index baf5de70..8cd7e5e9 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -565,17 +565,17 @@ def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id): cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - """SELECT mi.moduleimpl_id + """SELECT mi.id FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem - WHERE sem.formsemestre_id = %(formsemestre_id)s - AND mi.formsemestre_id = sem.formsemestre_id - AND mod.module_id = mi.module_id + WHERE sem.id = %(formsemestre_id)s + AND mi.formsemestre_id = sem.id + AND mod.id = mi.module_id AND mod.ue_id = %(ue_id)s """, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}, ) res = cursor.dictfetchall() - for moduleimpl_id in [x["moduleimpl_id"] for x in res]: + for moduleimpl_id in [x["id"] for x in res]: sco_moduleimpl.do_moduleimpl_inscription_create( {"moduleimpl_id": moduleimpl_id, "etudid": etudid}, formsemestre_id=formsemestre_id, From 18da8c9b93acda587006afc28866c29dd09ffb67 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 11:56:40 +0100 Subject: [PATCH 44/70] Fix: PE missing import --- app/pe/pe_tools.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/pe/pe_tools.py b/app/pe/pe_tools.py index 2c9dd230..5f58428b 100644 --- a/app/pe/pe_tools.py +++ b/app/pe/pe_tools.py @@ -35,13 +35,15 @@ Created on Thu Sep 8 09:36:33 2016 @author: barasc """ -from __future__ import print_function import os import datetime import re import unicodedata + +from flask import g + import app.scodoc.sco_utils as scu from app import log from app.scodoc.sco_logos import find_logo @@ -54,7 +56,6 @@ if not PE_DEBUG: # kw is ignored. log always add a newline log(" ".join(a)) - else: pe_print = print # print function @@ -206,7 +207,9 @@ def add_pe_stuff_to_zip(zipfile, ziproot): for name in logos_names: logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id) if logo is not None: - add_local_file_to_zip(zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename) + add_local_file_to_zip( + zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename + ) # ---------------------------------------------------------------------------------------- From d421088b526416fe8ff395eb6c865b98bea54036 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 12:40:56 +0100 Subject: [PATCH 45/70] Fix migrations order --- migrations/versions/c95d5a3bf0de_couleur_ue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/versions/c95d5a3bf0de_couleur_ue.py b/migrations/versions/c95d5a3bf0de_couleur_ue.py index f4dc60fc..c44dcdb8 100644 --- a/migrations/versions/c95d5a3bf0de_couleur_ue.py +++ b/migrations/versions/c95d5a3bf0de_couleur_ue.py @@ -1,7 +1,7 @@ """couleur UE Revision ID: c95d5a3bf0de -Revises: f40fbaf5831c +Revises: 28874ed6af64 Create Date: 2022-01-24 21:44:55.205544 """ @@ -11,7 +11,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "c95d5a3bf0de" -down_revision = "f40fbaf5831c" +down_revision = "28874ed6af64" branch_labels = None depends_on = None From e9c2c3c1f7aa0427e1e1541849e32c32e6e97181 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 14:32:00 +0100 Subject: [PATCH 46/70] Bonus: Le Mans, Mulhouse --- app/comp/bonus_spo.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index d442647a..d6abcb78 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -294,6 +294,29 @@ class BonusTours(BonusDirect): proportion_point = 1.0 / 40.0 +def bonus_iutlemans(notes_sport, coefs, infos=None): + # Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans + # La moyenne de chacune des UE du semestre sera majorée à hauteur de 2% du cumul des points supérieurs à 10 obtenus en matières optionnelles, + # dans la limite de 0,5 point. + points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10 + bonus = points * 0.02 # ou / 20 + return min(bonus, 0.5) # bonus limité à 0.5 point + + +class BonusLeMans(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans + + La moyenne de chacune des UE du semestre sera majorée à hauteur de + 2% du cumul des points supérieurs à 10 obtenus en matières optionnelles, + dans la limite de 0,5 point. + """ + + name = "bonus_iutlemans" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + proportion_point = 0.02 + bonus_moy_gen_limit = 0.5 # + + # Bonus simple, mais avec changement de paramètres en 2010 ! class BonusLille(BonusSportAdditif): """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq @@ -321,6 +344,20 @@ class BonusLille(BonusSportAdditif): ) +class BonusMulhouse(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT de Mulhouse + + La moyenne de chacune des UE du semestre sera majorée à hauteur de + 5% du cumul des points supérieurs à 10 obtenus en matières optionnelles, + dans la limite de 0,5 point. + """ + + name = "bonus_iutmulhouse" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + proportion_point = 0.05 + bonus_moy_gen_limit = 0.5 # + + class BonusSportMultiplicatif(BonusSport): """Bonus sport qui multiplie les moyennes d'UE par un facteur""" From 5c17410bcbe49135a3263198087bef4873bcf8ac Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 27 Jan 2022 18:12:40 +0100 Subject: [PATCH 47/70] Finition choix bonus sport --- app/comp/bonus_spo.py | 387 +++++++++++++++++++------------ app/forms/main/config_main.py | 4 +- app/models/config.py | 14 +- app/scodoc/bonus_sport.py | 2 +- app/scodoc/sco_edit_ue.py | 6 +- app/static/css/scodoc.css | 13 ++ app/static/js/edit_ue.js | 18 ++ app/templates/configuration.html | 17 +- app/views/scodoc.py | 14 +- 9 files changed, 316 insertions(+), 159 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index d6abcb78..e822394d 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -57,7 +57,7 @@ class BonusSport: # Attributs virtuels: seuil_moy_gen = None proportion_point = None - bonus_moy_gen_limit = None + bonus_max = None name = "virtual" @@ -217,147 +217,13 @@ class BonusSportAdditif(BonusSport): self.bonus_moy_gen = pd.Series( bonus_moy_gen_arr, index=self.etuds_idx, dtype=float ) - if self.bonus_moy_gen_limit is not None: - # Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points - self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit) + 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. -# bonus_ue = np.stack([modimpl_coefs_spo.T] * nb_ues) - - -class BonusIUTV(BonusSportAdditif): - """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Paris 13 (sports, musique, deuxième langue, - culture, etc) non rattachés à une unité d'enseignement. Les points - au-dessus de 10 sur 20 obtenus dans chacune des matières - optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à - la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - - name = "bonus_iutv" - pass # oui, c'ets le bonus par défaut - - -class BonusDirect(BonusSportAdditif): - """Bonus direct: les points sont directement ajoutés à la moyenne générale. - Les coefficients sont ignorés: tous les points de bonus sont sommés. - (rappel: la note est ramenée sur 20 avant application). - """ - - name = "bonus_direct" - seuil_moy_gen = 0.0 # tous les points sont comptés - proportion_point = 1.0 - - -class BonusStDenis(BonusIUTV): - """Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points.""" - - name = "bonus_iut_stdenis" - bonus_moy_gen_limit = 0.5 - - -class BonusColmar(BonusSportAdditif): - """Calcul bonus modules optionels (sport, culture), règle IUT Colmar. - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non - rattachés à une unité d'enseignement. Les points au-dessus de 10 - sur 20 obtenus dans chacune des matières optionnelles sont cumulés - dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à - la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - - # note: cela revient à dire que l'on ajoute 5% des points au dessus de 10, - # et qu'on limite à 5% de 10, soit 0.5 points - # ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis) - name = "bonus_colmar" - bonus_moy_gen_limit = 0.5 - - -class BonusTours(BonusDirect): - """Calcul bonus sport & culture IUT Tours. - - Les notes des UE bonus (ramenées sur 20) sont sommées - et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale, - soit pour le BUT à chaque moyenne d'UE. - - Le bonus total est limité à 1 point. - """ - - name = "bonus_tours" - bonus_moy_gen_limit = 1.0 # - seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés - proportion_point = 1.0 / 40.0 - - -def bonus_iutlemans(notes_sport, coefs, infos=None): - # Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans - # La moyenne de chacune des UE du semestre sera majorée à hauteur de 2% du cumul des points supérieurs à 10 obtenus en matières optionnelles, - # dans la limite de 0,5 point. - points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10 - bonus = points * 0.02 # ou / 20 - return min(bonus, 0.5) # bonus limité à 0.5 point - - -class BonusLeMans(BonusSportAdditif): - """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans - - La moyenne de chacune des UE du semestre sera majorée à hauteur de - 2% du cumul des points supérieurs à 10 obtenus en matières optionnelles, - dans la limite de 0,5 point. - """ - - name = "bonus_iutlemans" - seuil_moy_gen = 10.0 # points comptés au dessus de 10. - proportion_point = 0.02 - bonus_moy_gen_limit = 0.5 # - - -# Bonus simple, mais avec changement de paramètres en 2010 ! -class BonusLille(BonusSportAdditif): - """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq - - Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Lille 1 (sports, etc) non rattachés à une unité d'enseignement. - - Les points au-dessus de 10 sur 20 obtenus dans chacune des matières - optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés - s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant. - """ - - name = "bonus_lille" - seuil_moy_gen = 10.0 # points comptés au dessus de 10. - - def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): - """calcul du bonus""" - # La date du semestre ? - if self.formsemestre.date_debut > datetime.date(2010, 8, 1): - self.proportion_point = 0.04 - else: - self.proportion_point = 0.02 - return super().compute_bonus( - sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan - ) - - -class BonusMulhouse(BonusSportAdditif): - """Calcul bonus modules optionnels (sport, culture), règle IUT de Mulhouse - - La moyenne de chacune des UE du semestre sera majorée à hauteur de - 5% du cumul des points supérieurs à 10 obtenus en matières optionnelles, - dans la limite de 0,5 point. - """ - - name = "bonus_iutmulhouse" - seuil_moy_gen = 10.0 # points comptés au dessus de 10. - proportion_point = 0.05 - bonus_moy_gen_limit = 0.5 # - - class BonusSportMultiplicatif(BonusSport): """Bonus sport qui multiplie les moyennes d'UE par un facteur""" @@ -381,8 +247,12 @@ class BonusSportMultiplicatif(BonusSport): factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20 factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus - # S'applique qu'aux moyennes d'UE + # Ne s'applique qu'aux moyennes d'UE bonus = self.etud_moy_ue * factor + if self.bonus_max is not None: + # Seuil: bonus limité à bonus_max points + bonus.clip(upper=self.bonus_max, inplace=True) + self.bonus_ues = bonus # DataFrame if not self.formsemestre.formation.is_apc(): @@ -390,6 +260,91 @@ class BonusSportMultiplicatif(BonusSport): self.bonus_moy_gen = bonus +class BonusDirect(BonusSportAdditif): + """Bonus direct: les points sont directement ajoutés à la moyenne générale. + Les coefficients sont ignorés: tous les points de bonus sont sommés. + (rappel: la note est ramenée sur 20 avant application). + """ + + name = "bonus_direct" + displayed_name = 'Bonus "direct"' + seuil_moy_gen = 0.0 # tous les points sont comptés + proportion_point = 1.0 + + +class BonusBethune(BonusSportMultiplicatif): + """Calcul bonus modules optionels (sport), règle IUT de Béthune. + + 5% des points au dessus de 10., limité à 0.5 point de bonus. + """ + + name = "bonus_iutbethune" + displayed_name = "IUT de Béthune" + seuil_moy_gen = 10.0 + amplitude = 0.005 + bonus_max = 0.5 # plafonnement à 0.5 points + + +class BonusBezier(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT de Bézier. + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + sport , etc) non rattachés à une unité d'enseignement. Les points + au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 3% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant, dans + la limite de 0,3 points. + """ + + # note: cela revient à dire que l'on ajoute 5% des points au dessus de 10, + # et qu'on limite à 5% de 10, soit 0.5 points + # ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis) + name = "bonus_iutbeziers" + displayed_name = "IUT de Bézier" + bonus_max = 0.3 + seuil_moy_gen = 10.0 # tous les points sont comptés + proportion_point = 0.03 + + +class BonusBordeaux1(BonusSportMultiplicatif): + """Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UE + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement. + En cas de double activité, c'est la meilleure des 2 notes qui compte. + Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un % + qui augmente la moyenne de chaque UE et la moyenne générale. + Formule : le % = points>moyenne / 2 + Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale. + + Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées. + + """ + + pass # XXX en attente de Cédric + + +class BonusColmar(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Colmar. + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non + rattachés à une unité d'enseignement. Les points au-dessus de 10 + sur 20 obtenus dans chacune des matières optionnelles sont cumulés + dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + # note: cela revient à dire que l'on ajoute 5% des points au dessus de 10, + # et qu'on limite à 5% de 10, soit 0.5 points + # ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis) + name = "bonus_colmar" + displayed_name = "IUT de Colmar" + bonus_max = 0.5 + seuil_moy_gen = 10.0 # tous les points sont comptés + proportion_point = 0.05 + + class BonusGrenobleIUT1(BonusSportMultiplicatif): """Bonus IUT1 de Grenoble @@ -409,6 +364,7 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif): """ name = "bonus_iut1grenoble_2017" + displayed_name = "IUT de Grenoble 1" # C'est un bonus "multiplicatif": on l'exprime en additif, # sur chaque moyenne d'UE m_0 # Augmenter de 5% correspond à multiplier par a=1.05 @@ -428,6 +384,20 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif): super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan) +class BonusLaRochelle(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT de La Rochelle. + + Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point. + Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette + note sur la moyenne générale du semestre (ou sur les UE en BUT). + """ + + name = "bonus_iutlr" + displayed_name = "IUT de La Rochelle" + seuil_moy_gen = 10.0 # tous les points sont comptés + proportion_point = 0.01 + + class BonusLeHavre(BonusSportMultiplicatif): """Bonus sport IUT du Havre sur moyenne générale et UE @@ -436,10 +406,84 @@ class BonusLeHavre(BonusSportMultiplicatif): """ name = "bonus_iutlh" + displayed_name = "IUT du Havre" seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés amplitude = 0.005 # multiplie les points au dessus du seuil +class BonusLeMans(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans + + La moyenne de chacune des UE du semestre sera majorée à hauteur de + 2% du cumul des points supérieurs à 10 obtenus en matières optionnelles, + dans la limite de 0,5 point. + """ + + name = "bonus_iutlemans" + displayed_name = "IUT du Mans" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + proportion_point = 0.02 + bonus_max = 0.5 # + + +# Bonus simple, mais avec changement de paramètres en 2010 ! +class BonusLille(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Lille 1 (sports, etc) non rattachés à une unité d'enseignement. + + Les points au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés + s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + name = "bonus_lille" + displayed_name = "IUT de Lille" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # La date du semestre ? + if self.formsemestre.date_debut > datetime.date(2010, 8, 1): + self.proportion_point = 0.04 + else: + self.proportion_point = 0.02 + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) + + +class BonusLyonProvisoire(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT de Lyon (provisoire) + + Les points au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 1,8% de ces points cumulés + s'ajoutent aux moyennes, dans la limite d'1/2 point. + """ + + name = "bonus_lyon_provisoire" + displayed_name = "IUT de Lyon (provisoire)" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + proportion_point = 0.018 + bonus_max = 0.5 + + +class BonusMulhouse(BonusSportAdditif): + """Calcul bonus modules optionnels (sport, culture), règle IUT de Mulhouse + + La moyenne de chacune des UE du semestre sera majorée à hauteur de + 5% du cumul des points supérieurs à 10 obtenus en matières optionnelles, + dans la limite de 0,5 point. + """ + + name = "bonus_iutmulhouse" + displayed_name = "IUT de Mulhouse" + seuil_moy_gen = 10.0 # points comptés au dessus de 10. + proportion_point = 0.05 + bonus_max = 0.5 # + + class BonusNantes(BonusSportAdditif): """IUT de Nantes (Septembre 2018) @@ -457,24 +501,60 @@ class BonusNantes(BonusSportAdditif): """ name = "bonus_nantes" + displayed_name = "IUT de Nantes" seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés proportion_point = 1 # multiplie les points au dessus du seuil - bonus_moy_gen_limit = 0.5 # plafonnement à 0.5 points + bonus_max = 0.5 # plafonnement à 0.5 points class BonusRoanne(BonusSportAdditif): """IUT de Roanne. - Le bonus est compris entre 0 et 0.35 point + Le bonus est compris entre 0 et 0.6 points et est toujours appliqué aux UEs. """ name = "bonus_iutr" + displayed_name = "IUT de Roanne" seuil_moy_gen = 0.0 - bonus_moy_gen_limit = 0.35 # plafonnement à 0.35 points + bonus_max = 0.6 # plafonnement à 0.6 points apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP +class BonusStDenis(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Saint-Denis + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Paris 13 (sports, musique, deuxième langue, + culture, etc) non rattachés à une unité d'enseignement. Les points + au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant, dans la limite + d'1/2 point. + """ + + name = "bonus_iut_stdenis" + displayed_name = "IUT de Saint-Denis" + bonus_max = 0.5 + + +class BonusTours(BonusDirect): + """Calcul bonus sport & culture IUT Tours. + + Les notes des UE bonus (ramenées sur 20) sont sommées + et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale, + soit pour le BUT à chaque moyenne d'UE. + + Le bonus total est limité à 1 point. + """ + + name = "bonus_tours" + displayed_name = "IUT de Tours" + bonus_max = 1.0 # + seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés + proportion_point = 1.0 / 40.0 + + class BonusVilleAvray(BonusSport): """Bonus modules optionels (sport, culture), règle IUT Ville d'Avray. @@ -488,6 +568,7 @@ class BonusVilleAvray(BonusSport): """ name = "bonus_iutva" + displayed_name = "IUT de Ville d'Avray" def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): """calcul du bonus""" @@ -503,13 +584,29 @@ class BonusVilleAvray(BonusSport): self.bonus_moy_gen = pd.Series( bonus_moy_gen_arr, index=self.etuds_idx, dtype=float ) - if self.bonus_moy_gen_limit is not None: - # Seuil: bonus (sur moy. gen.) limité à bonus_moy_gen_limit points - self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_moy_gen_limit) + 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. +class BonusIUTV(BonusSportAdditif): + """Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse + + Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université Paris 13 (sports, musique, deuxième langue, + culture, etc) non rattachés à une unité d'enseignement. Les points + au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à + la moyenne générale du semestre déjà obtenue par l'étudiant. + """ + + name = "bonus_iutv" + displayed_name = "IUT de Villetaneuse" + pass # oui, c'ets le bonus par défaut + + def get_bonus_class_dict(start=BonusSport, d=None): """Dictionnaire des classes de bonus (liste les sous-classes de BonusSport ayant un nom) diff --git a/app/forms/main/config_main.py b/app/forms/main/config_main.py index e0b349e4..d6536a8d 100644 --- a/app/forms/main/config_main.py +++ b/app/forms/main/config_main.py @@ -42,8 +42,8 @@ class ScoDocConfigurationForm(FlaskForm): bonus_sport_func_name = SelectField( label="Fonction de calcul des bonus sport&culture", choices=[ - (x, x if x else "Aucune") - for x in ScoDocSiteConfig.get_bonus_sport_class_names() + (name, displayed_name if name else "Aucune") + for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list() ], ) submit = SubmitField("Valider") diff --git a/app/models/config.py b/app/models/config.py index da98998c..9c9c5638 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -124,7 +124,7 @@ class ScoDocSiteConfig(db.Model): If class_name not found in module bonus_sport, returns None and flash a warning. """ - if class_name is None: + if not class_name: # None or "" c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first() if c is None: return None @@ -140,12 +140,22 @@ class ScoDocSiteConfig(db.Model): return klass @classmethod - def get_bonus_sport_class_names(cls): + def get_bonus_sport_class_names(cls) -> list: """List available bonus class names (starting with empty string to represent "no bonus function"). """ return [""] + sorted(bonus_spo.get_bonus_class_dict().keys()) + @classmethod + def get_bonus_sport_class_list(cls) -> list[tuple]: + """List available bonus class names + (starting with empty string to represent "no bonus function"). + """ + d = bonus_spo.get_bonus_class_dict() + class_list = [(name, d[name].displayed_name) for name in d.keys()] + class_list.sort(key=lambda x: x[1].replace(" du ", " de ")) + return [("", "")] + class_list + @classmethod def get_bonus_sport_func(cls): """Fonction bonus_sport ScoDoc 7 XXX diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 1e754f82..5fc3b8b4 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -375,7 +375,7 @@ def bonus_iutBordeaux1(notes_sport, coefs, infos=None): return bonus -def bonus_iuto(notes_sport, coefs, infos=None): +def bonus_iuto(notes_sport, coefs, infos=None): # OBSOLETE => EN ATTENTE (27/01/2022) """Calcul bonus modules optionels (sport, culture), règle IUT Orleans * Avant aout 2013 Un bonus de 2,5% de la note de sport est accordé à chaque UE sauf diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 788f4b93..5c4741f7 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -390,9 +390,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None): submitlabel=submitlabel, ) if tf[0] == 0: - X = """
- """ - return "\n".join(H) + tf[1] + X + html_sco_header.sco_footer() + ue_div = """
""" + bonus_div = """
""" + return "\n".join(H) + tf[1] + bonus_div + ue_div + html_sco_header.sco_footer() else: if create: if not tf[2]["ue_code"]: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 684df8a0..c5758cb5 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -881,6 +881,19 @@ div.sco_help { span.wtf-field ul.errors li { color: red; } + +#bonus_description { + color:rgb(6, 73, 6); + padding: 5px; + margin-top:5px; + border: 2px solid blue; + border-radius: 5px; + background-color: cornsilk; +} +#bonus_description div.bonus_description_head{ + font-weight: bold; +} + .configuration_logo div.img { } diff --git a/app/static/js/edit_ue.js b/app/static/js/edit_ue.js index 34e8e9c8..8424496c 100644 --- a/app/static/js/edit_ue.js +++ b/app/static/js/edit_ue.js @@ -3,8 +3,26 @@ $().ready(function () { update_ue_list(); $("#tf_ue_code").bind("keyup", update_ue_list); + + $("select#tf_type").change(function () { + update_bonus_description(); + }); + update_bonus_description(); }); +function update_bonus_description() { + var ue_type = $("#tf_type")[0].value; + if (ue_type == "1") { /* UE SPORT */ + $("#bonus_description").show(); + var query = "/ScoDoc/get_bonus_description/default"; + $.get(query, '', function (data) { + $("#bonus_description").html(data); + }); + } else { + $("#bonus_description").html(""); + $("#bonus_description").hide(); + } +} function update_ue_list() { var ue_id = $("#tf_ue_id")[0].value; diff --git a/app/templates/configuration.html b/app/templates/configuration.html index 888d9591..823772de 100644 --- a/app/templates/configuration.html +++ b/app/templates/configuration.html @@ -30,8 +30,8 @@
{{ wtf.quick_form(form) }}
-
+

Gestion des images: logos, signatures, ...

Ces images peuvent être intégrées dans les documents @@ -51,14 +51,21 @@ {{ super() }} {% endblock %} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 234d688c..0b37da54 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -32,6 +32,7 @@ Emmanuel Viennet, 2021 """ import datetime import io +import re import flask from flask import abort, flash, url_for, redirect, render_template, send_file @@ -259,8 +260,19 @@ def configuration(): @bp.route("/ScoDoc/get_bonus_description/", methods=["GET"]) def get_bonus_description(bonus_name: str): "description text/html du bonus" + if bonus_name == "default": + bonus_name = "" bonus_class = ScoDocSiteConfig.get_bonus_sport_class_from_name(bonus_name) - return bonus_class.__doc__ + text = bonus_class.__doc__ + fields = re.split(r"\n\n", text, maxsplit=1) + if len(fields) > 1: + first_line, text = fields + else: + first_line, text = "", fields[0] + + return f"""
{first_line}
+
{text}
+ """ @bp.route("/ScoDoc/configure_logos", methods=["GET", "POST"]) From 32af5a0dc952eff802c6c2ebf2fdfc5b4f5d0551 Mon Sep 17 00:00:00 2001 From: Yann Leboulanger Date: Thu, 27 Jan 2022 21:02:54 +0100 Subject: [PATCH 48/70] logo path has changed --- tools/doc_poursuites_etudes/distrib/modeles/un_avis.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/doc_poursuites_etudes/distrib/modeles/un_avis.tex b/tools/doc_poursuites_etudes/distrib/modeles/un_avis.tex index 05a6320f..302568fd 100644 --- a/tools/doc_poursuites_etudes/distrib/modeles/un_avis.tex +++ b/tools/doc_poursuites_etudes/distrib/modeles/un_avis.tex @@ -17,7 +17,7 @@ % ************************************************************ % En-tête de l'avis % ************************************************************ -\begin{entete}{logos/logo_header} +\begin{entete}{logos/header} \textbf{\Huge{Avis de Poursuites d'Etudes}} \\ \ligne \\ \normalsize{Département **DeptFullName**} \\ From 7e4459a15ed4192103f991166e5b9bfdda61a83e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 28 Jan 2022 00:22:36 +0100 Subject: [PATCH 49/70] Bonus Bordeaux --- app/but/bulletin_but.py | 2 +- app/comp/bonus_spo.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 5df15806..11eb7e65 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -168,7 +168,7 @@ class BulletinBUT(ResultatsSemestreBUT): return "" import random - bonus_vect = self.bonus_ues.loc[etudid] + [random.random() for i in range(3)] + bonus_vect = self.bonus_ues.loc[etudid] if bonus_vect.nunique() > 1: # détail UE par UE details = [ diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index e822394d..9be542c5 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -275,14 +275,15 @@ class BonusDirect(BonusSportAdditif): class BonusBethune(BonusSportMultiplicatif): """Calcul bonus modules optionels (sport), règle IUT de Béthune. - 5% des points au dessus de 10., limité à 0.5 point de bonus. + Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre. + Ce bonus est égal au nombre de points divisé par 200 et multiplié par la + moyenne générale du semestre de l'étudiant. """ name = "bonus_iutbethune" displayed_name = "IUT de Béthune" seuil_moy_gen = 10.0 amplitude = 0.005 - bonus_max = 0.5 # plafonnement à 0.5 points class BonusBezier(BonusSportAdditif): @@ -307,21 +308,23 @@ class BonusBezier(BonusSportAdditif): class BonusBordeaux1(BonusSportMultiplicatif): - """Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UE + """Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale + et UE. Les étudiants de l'IUT peuvent suivre des enseignements optionnels de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement. - En cas de double activité, c'est la meilleure des 2 notes qui compte. + Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale. Formule : le % = points>moyenne / 2 Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale. - Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées. - """ - pass # XXX en attente de Cédric + name = "bonus_iutBordeaux1" + displayed_name = "IUT de Bordeaux 1" + seuil_moy_gen = 10.0 + amplitude = 0.005 class BonusColmar(BonusSportAdditif): From 23672bebded9484518e2ef60fd47d6b4a2f56676 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 29 Jan 2022 22:45:39 +0100 Subject: [PATCH 50/70] Bonus sport multiplicatifs ou additifs sur bulletins DUT et BUT --- app/but/bulletin_but.py | 32 +++++++----- app/comp/bonus_spo.py | 99 ++++++++++++++++++++----------------- app/comp/moy_mod.py | 8 ++- app/comp/moy_ue.py | 35 ++++++++++--- app/comp/res_but.py | 50 +++++++++++-------- app/comp/res_common.py | 37 ++++++++------ app/models/formations.py | 13 +++++ app/scodoc/sco_bulletins.py | 24 +++++---- 8 files changed, 186 insertions(+), 112 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 11eb7e65..b9e33478 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -28,21 +28,29 @@ class BulletinBUT(ResultatsSemestreBUT): "dict synthèse résultats dans l'UE pour les modules indiqués" d = {} etud_idx = self.etud_index[etud.id] - ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id) + if ue.type != UE_SPORT: + ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id) etud_moy_module = self.sem_cube[etud_idx] # module x UE for modimpl in modimpls: if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit - coef = self.modimpl_coefs_df[modimpl.id][ue.id] - if coef > 0: - d[modimpl.module.code] = { - "id": modimpl.id, - "coef": coef, - "moyenne": fmt_note( - etud_moy_module[ - self.modimpl_coefs_df.columns.get_loc(modimpl.id) - ][ue_idx] - ), - } + if ue.type != UE_SPORT: + coef = self.modimpl_coefs_df[modimpl.id][ue.id] + if coef > 0: + d[modimpl.module.code] = { + "id": modimpl.id, + "coef": coef, + "moyenne": fmt_note( + etud_moy_module[ + self.modimpl_coefs_df.columns.get_loc(modimpl.id) + ][ue_idx] + ), + } + # else: # modules dans UE bonus sport + # d[modimpl.module.code] = { + # "id": modimpl.id, + # "coef": "", + # "moyenne": "?x?", + # } return d def etud_ue_results(self, etud, ue): diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 9be542c5..e9f615cf 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -38,7 +38,7 @@ class BonusSport: notes moyennes aux modules (tous les étuds x tous les modimpls) floats avec des NaN. En classique: sem_matrix, ndarray (etuds x modimpls) - En APC: sem_cube, ndarray (etuds x modimpls x UEs) + En APC: sem_cube, ndarray (etuds x modimpls x UEs non bonus) - ues: les ues du semestre (incluant le bonus sport) - modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl) - modimpl_coefs: les coefs des modules @@ -50,10 +50,9 @@ class BonusSport: etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs). """ - # Si vrai, en APC, si le bonus UE est None, reporte le bonus moy gen: - apc_apply_bonus_mg_to_ues = True - # Si True, reporte toujours le bonus moy gen sur les UE (même en formations classiques) - apply_bonus_mg_to_ues = False + # En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen reste None) + classic_use_bonus_ues = False + # Attributs virtuels: seuil_moy_gen = None proportion_point = None @@ -77,7 +76,7 @@ class BonusSport: self.etud_moy_ue = etud_moy_ue self.etuds_idx = modimpl_inscr_df.index # les étudiants inscrits au semestre self.bonus_ues: pd.DataFrame = None # virtual - self.bonus_moy_gen: pd.Series = None # virtual + self.bonus_moy_gen: pd.Series = None # virtual (pour formations non apc slt) # Restreint aux modules standards des UE de type "sport": modimpl_mask = np.array( [ @@ -94,13 +93,14 @@ class BonusSport: "liste des modimpls sport" # Les moyennes des modules "sport": (une par UE en APC) + # donc (nb_etuds, nb_mod_sport, nb_ues_non_bonus) sem_modimpl_moys_spo = sem_modimpl_moys[:, modimpl_mask] # Les inscriptions aux modules sport: modimpl_inscr_spo = modimpl_inscr_df.values[:, modimpl_mask] - # Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue) + # Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue) (toutes ues) modimpl_coefs_spo = modimpl_coefs[modimpl_mask] # sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport) - # ou (nb_etuds, nb_mod_sport, nb_ues) + # ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus) nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2] nb_ues = len(ues) # Enlève les NaN du numérateur: @@ -115,7 +115,7 @@ class BonusSport: [modimpl_inscr_spo] * nb_ues_no_bonus, axis=2 ) # Ne prend pas en compte les notes des étudiants non inscrits au module: - # Annule les notes: + # Annule les notes: (nb_etuds, nb_mod_bonus, nb_ues_non_bonus) sem_modimpl_moys_inscrits = np.where( modimpl_inscr_spo_stacked, sem_modimpl_moys_no_nan, 0.0 ) @@ -151,7 +151,7 @@ class BonusSport: """Calcul des bonus: méthode virtuelle à écraser. Arguments: - sem_modimpl_moys_inscrits: - ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue) + ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue_non_bonus) les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans. - modimpl_coefs_etuds_no_nan: les coefficients: float ndarray @@ -164,24 +164,16 @@ class BonusSport: """Les bonus à appliquer aux UE Résultat: DataFrame de float, index etudid, columns: ue.id """ - if self.bonus_ues is None and ( - (self.apc_apply_bonus_mg_to_ues and self.formsemestre.formation.is_apc()) - or self.apply_bonus_mg_to_ues - ): - # reporte uniformément le bonus moyenne générale sur les UEs - # (assure la compatibilité de la plupart des anciens bonus avec le BUT) - # ues = self.formsemestre.query_ues(with_sport=False) - ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)] - bonus_moy_gen = self.get_bonus_moy_gen() - bonus_ues = np.stack([bonus_moy_gen.values] * len(ues_idx), axis=1) - return pd.DataFrame(bonus_ues, index=self.etuds_idx, columns=ues_idx) - - return self.bonus_ues + if self.classic_use_bonus_ues or self.formsemestre.formation.is_apc(): + return self.bonus_ues + return None def get_bonus_moy_gen(self): """Le bonus à appliquer à la moyenne générale. Résultat: Series de float, index etudid """ + if self.formsemestre.formation.is_apc(): + return None # garde-fou return self.bonus_moy_gen @@ -200,8 +192,12 @@ class BonusSportAdditif(BonusSport): proportion_point = 0.05 # multiplie les points au dessus du seuil def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): - """calcul du bonus""" - bonus_moy_gen_arr = np.sum( + """calcul du bonus + sem_modimpl_moys_inscrits: les notes de sport + En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus) + modimpl_coefs_etuds_no_nan: + """ + bonus_moy_arr = np.sum( np.where( sem_modimpl_moys_inscrits > self.seuil_moy_gen, (sem_modimpl_moys_inscrits - self.seuil_moy_gen) @@ -210,18 +206,28 @@ class BonusSportAdditif(BonusSport): ), axis=1, ) - # en APC, applati la moyenne gen. XXX pourrait être fait en amont - if len(bonus_moy_gen_arr.shape) > 1: - bonus_moy_gen_arr = bonus_moy_gen_arr.sum(axis=1) - # Bonus moyenne générale, et 0 sur les UE - self.bonus_moy_gen = pd.Series( - bonus_moy_gen_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) + # Seuil: bonus limité à bonus_max points (et >= 0) + bonus_moy_arr = np.clip( + bonus_moy_arr, 0.0, self.bonus_max, out=bonus_moy_arr + ) - # Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs. + # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus) + if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues: + # Bonus sur les UE et None sur moyenne générale + ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)] + self.bonus_ues = pd.DataFrame( + bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float + ) + else: + # Bonus sur la moyenne générale seulement + self.bonus_moy_gen = pd.Series( + bonus_moy_arr, index=self.etuds_idx, dtype=float + ) + + # if len(bonus_moy_arr.shape) > 1: + # bonus_moy_arr = bonus_moy_arr.sum(axis=1) + # Laisse bonus_moy_gen à None, en APC le bonus moy. gen. sera réparti sur les UEs. class BonusSportMultiplicatif(BonusSport): @@ -229,6 +235,8 @@ class BonusSportMultiplicatif(BonusSport): seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés amplitude = 0.005 # multiplie les points au dessus du seuil + # En classique, les bonus multiplicatifs agissent par défaut sur les UE: + classic_use_bonus_ues = True # C'est un bonus "multiplicatif": on l'exprime en additif, # sur chaque moyenne d'UE m_0 @@ -243,11 +251,12 @@ class BonusSportMultiplicatif(BonusSport): sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) notes = np.nan_to_num(notes, copy=False) - factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20 factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus # Ne s'applique qu'aux moyennes d'UE + if len(factor.shape) == 1: # classic + factor = factor[:, np.newaxis] bonus = self.etud_moy_ue * factor if self.bonus_max is not None: # Seuil: bonus limité à bonus_max points @@ -255,9 +264,8 @@ class BonusSportMultiplicatif(BonusSport): self.bonus_ues = bonus # DataFrame - if not self.formsemestre.formation.is_apc(): - # s'applique à la moyenne générale - self.bonus_moy_gen = bonus + # Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale + self.bonus_moy_gen = None class BonusDirect(BonusSportAdditif): @@ -323,6 +331,7 @@ class BonusBordeaux1(BonusSportMultiplicatif): name = "bonus_iutBordeaux1" displayed_name = "IUT de Bordeaux 1" + classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP seuil_moy_gen = 10.0 amplitude = 0.005 @@ -576,17 +585,15 @@ class BonusVilleAvray(BonusSport): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): """calcul du bonus""" # Calcule moyenne pondérée des notes de sport: - bonus_moy_gen_arr = np.sum( + 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_gen_arr[bonus_moy_gen_arr >= 10.0] = 0.1 - bonus_moy_gen_arr[bonus_moy_gen_arr >= 12.0] = 0.2 - bonus_moy_gen_arr[bonus_moy_gen_arr >= 16.0] = 0.3 + 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 >= 16.0] = 0.3 # Bonus moyenne générale, et 0 sur les UE - self.bonus_moy_gen = pd.Series( - bonus_moy_gen_arr, index=self.etuds_idx, dtype=float - ) + 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) diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 1d8ff453..1a64809f 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -40,6 +40,7 @@ import pandas as pd from app import db from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.scodoc import sco_utils as scu +from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_exceptions import ScoValueError @@ -269,7 +270,8 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: """Charge poids des évaluations d'un module et retourne un dataframe rows = evaluations, columns = UE, value = poids (float). Les valeurs manquantes (évaluations sans coef vers des UE) sont - remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon. + remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon + (sauf pour module bonus, defaut à 1) Résultat: (evals_poids, liste de UEs du semestre sauf le sport) """ modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id) @@ -287,11 +289,13 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... # Initialise poids non enregistrés: + default_poids = 1.0 if modimpl.module.ue.type == UE_SPORT else 0.0 + if np.isnan(evals_poids.values.flat).any(): ue_coefs = modimpl.module.get_ue_coef_dict() for ue in ues: evals_poids[ue.id][evals_poids[ue.id].isna()] = ( - 1 if ue_coefs.get(ue.id, 0.0) > 0 else 0 + 1 if ue_coefs.get(ue.id, default_poids) > 0 else 0 ) return evals_poids, ues diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index cee1b888..289822ff 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -36,6 +36,7 @@ from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef from app.comp import moy_mod from app.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours +from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -44,10 +45,10 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data En APC, ces coefs lient les modules à chaque UE. - Résultat: (module_coefs_df, ues, modules) + Résultat: (module_coefs_df, ues_no_bonus, modules) DataFrame rows = UEs, columns = modules, value = coef. - Considère toutes les UE (sauf sport) et modules du semestre. + Considère toutes les UE sauf bonus et tous les modules du semestre. Les coefs non définis (pas en base) sont mis à zéro. Si semestre_idx None, prend toutes les UE de la formation. @@ -92,7 +93,17 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef # silently ignore coefs associated to other modules (ie when module_type is changed) - module_coefs_df.fillna(value=0, inplace=True) + # Initialisation des poids non fixés: + # 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse + # sur toutes les UE) + default_poids = { + mod.id: 1.0 + if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT) + else 0.0 + for mod in modules + } + + module_coefs_df.fillna(value=default_poids, inplace=True) return module_coefs_df, ues, modules @@ -104,9 +115,9 @@ def df_load_modimpl_coefs( Comme df_load_module_coefs mais prend seulement les UE et modules du formsemestre. - Si ues et modimpls sont None, prend tous ceux du formsemestre. + Si ues et modimpls sont None, prend tous ceux du formsemestre (sauf ue bonus). Résultat: (module_coefs_df, ues, modules) - DataFrame rows = UEs (avec bonus), columns = modimpl, value = coef. + DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef. """ if ues is None: ues = formsemestre.query_ues().all() @@ -124,7 +135,19 @@ def df_load_modimpl_coefs( for mod_coef in mod_coefs: modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef - modimpl_coefs_df.fillna(value=0, inplace=True) + + # Initialisation des poids non fixés: + # 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse + # sur toutes les UE) + default_poids = { + modimpl.id: 1.0 + if (modimpl.module.module_type == ModuleType.STANDARD) + and (modimpl.module.ue.type == UE_SPORT) + else 0.0 + for modimpl in formsemestre.modimpls_sorted + } + + modimpl_coefs_df.fillna(value=default_poids, inplace=True) return modimpl_coefs_df, ues, modimpls diff --git a/app/comp/res_but.py b/app/comp/res_but.py index e7c6d4c4..266cda97 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -40,22 +40,22 @@ class ResultatsSemestreBUT(NotesTableCompat): ) = moy_ue.notes_sem_load_cube(self.formsemestre) self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre) self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs( - self.formsemestre, ues=self.ues, modimpls=self.formsemestre.modimpls_sorted + self.formsemestre, modimpls=self.formsemestre.modimpls_sorted ) # l'idx de la colonne du mod modimpl.id est # modimpl_coefs_df.columns.get_loc(modimpl.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) - # Elimine les coefs des UE bonus sports - no_bonus = [ue.type != UE_SPORT for ue in self.ues] - modimpl_coefs_no_bonus_df = self.modimpl_coefs_df[no_bonus] + # Elimine les coefs des UE bonus sports XXX inutile car df_load_modimpl_coefs sans bonus + # no_bonus = [ue.type != UE_SPORT for ue in self.ues] + # modimpl_coefs_no_bonus_df = self.modimpl_coefs_df[no_bonus] self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.sem_cube, self.etuds, self.formsemestre.modimpls_sorted, self.ues, self.modimpl_inscr_df, - modimpl_coefs_no_bonus_df, + self.modimpl_coefs_df, ) # Les coefficients d'UE ne sont pas utilisés en APC self.etud_coef_ue_df = pd.DataFrame( @@ -63,25 +63,33 @@ class ResultatsSemestreBUT(NotesTableCompat): ) # --- Bonus Sport & Culture - bonus_class = ScoDocSiteConfig.get_bonus_sport_class() - if bonus_class is not None: - bonus: BonusSport = bonus_class( - self.formsemestre, - self.sem_cube, - self.ues, - self.modimpl_inscr_df, - self.modimpl_coefs_df.transpose(), - self.etud_moy_gen, - self.etud_moy_ue, - ) - self.bonus_ues = bonus.get_bonus_ues() - if self.bonus_ues is not None: - self.etud_moy_ue += self.bonus_ues # somme les dataframes - self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) + modimpl_sport = [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if modimpl.module.ue.type == UE_SPORT + ] + if len(modimpl_sport) > 0: + bonus_class = ScoDocSiteConfig.get_bonus_sport_class() + if bonus_class is not None: + bonus: BonusSport = bonus_class( + self.formsemestre, + self.sem_cube, + self.ues, + self.modimpl_inscr_df, + self.modimpl_coefs_df.transpose(), + self.etud_moy_gen, + self.etud_moy_ue, + ) + self.bonus_ues = bonus.get_bonus_ues() + if self.bonus_ues is not None: + self.etud_moy_ue += self.bonus_ues # somme les dataframes + self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True) # Moyenne générale indicative: + # (note: le bonus sport a déjà été appliqué aux moyenens d'UE, et impacte + # donc la moyenne indicative) self.etud_moy_gen = moy_sem.compute_sem_moys_apc( - self.etud_moy_ue, modimpl_coefs_no_bonus_df + self.etud_moy_ue, self.modimpl_coefs_df ) self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index e2e5ab16..a90ce1c1 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -219,18 +219,18 @@ class NotesTableCompat(ResultatsSemestre): ues.append(d) return ues - def get_modimpls_dict(self, ue_id=None): + def get_modimpls_dict(self, ue_id=None) -> list[dict]: """Liste des modules pour une UE (ou toutes si ue_id==None), triés par numéros (selon le type de formation) """ - if ue_id is None: - return [m.to_dict() for m in self.formsemestre.modimpls_sorted] - else: - return [ - m.to_dict() - for m in self.formsemestre.modimpls_sorted - if m.module.ue.id == ue_id - ] + modimpls_dict = [] + for modimpl in self.formsemestre.modimpls_sorted: + if ue_id == None or modimpl.module.ue.id == ue_id: + d = modimpl.to_dict() + # compat ScoDoc < 9.2: ajoute matières + d["mat"] = modimpl.module.matiere.to_dict() + modimpls_dict.append(d) + return modimpls_dict def get_etud_decision_sem(self, etudid: int) -> dict: """Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu. @@ -259,13 +259,10 @@ class NotesTableCompat(ResultatsSemestre): return "" return ins.etat - def get_etud_moy_gen(self, etudid): # -> float | str - """Moyenne générale de cet etudiant dans ce semestre. - Prend(ra) en compte les UE capitalisées. (TODO) XXX - Si apc, moyenne indicative. - Si pas de notes: 'NA' - """ - return self.etud_moy_gen[etudid] + def get_etud_mat_moy(self, matiere_id, etudid): + """moyenne d'un étudiant dans une matière (ou NA si pas de notes)""" + # non supporté en 9.2 + return "na" def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float: """La moyenne de l'étudiant dans le moduleimpl @@ -274,6 +271,14 @@ class NotesTableCompat(ResultatsSemestre): """ raise NotImplementedError() # virtual method + def get_etud_moy_gen(self, etudid): # -> float | str + """Moyenne générale de cet etudiant dans ce semestre. + Prend(ra) en compte les UE capitalisées. (TODO) XXX + Si apc, moyenne indicative. + Si pas de notes: 'NA' + """ + return self.etud_moy_gen[etudid] + def get_etud_ue_status(self, etudid: int, ue_id: int): coef_ue = self.etud_coef_ue_df[ue_id][etudid] return { diff --git a/app/models/formations.py b/app/models/formations.py index b69d566a..edd57097 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -161,3 +161,16 @@ class Matiere(db.Model): numero = db.Column(db.Integer) # ordre de présentation modules = db.relationship("Module", lazy="dynamic", backref="matiere") + + def __repr__(self): + return f"""<{self.__class__.__name__}(id={self.id}, ue_id={ + self.ue_id}, titre='{self.titre}')>""" + + def to_dict(self): + """as a dict, with the same conversions as in ScoDoc7""" + 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 + return e diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 4dcf4d81..23833f88 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -310,7 +310,10 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): else: x = "" if isinstance(x, str): - u["cur_moy_ue_txt"] = "pas de bonus" + if nt.bonus_ues is None: + u["cur_moy_ue_txt"] = "pas de bonus" + else: + u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs" else: u["cur_moy_ue_txt"] = "bonus de %.3g points" % x u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) @@ -380,7 +383,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): ) else: if prefs["bul_show_ue_rangs"] and ue["type"] != sco_codes_parcours.UE_SPORT: - if ue_attente: # nt.get_moduleimpls_attente(): + if ue_attente or nt.ue_rangs[ue["ue_id"]][0] is None: u["ue_descr_txt"] = "%s/%s" % ( scu.RANG_ATTENTE_STR, nt.ue_rangs[ue["ue_id"]][1], @@ -398,8 +401,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"): I["ues"].append(u) # ne montre pas les UE si non inscrit # Accès par matieres - # voir si on supporte encore cela en #sco92 XXX - # I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid)) + # En #sco92, pas d'information + I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid)) # C = make_context_dict(I["sem"], I["etud"]) @@ -616,12 +619,15 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version): # Classement if bul_show_mod_rangs and mod["mod_moy_txt"] != "-" and not is_malus: rg = nt.mod_rangs[modimpl["moduleimpl_id"]] - if mod_attente: # nt.get_moduleimpls_attente(): - mod["mod_rang"] = scu.RANG_ATTENTE_STR + if rg[0] is None: + mod["mod_rang_txt"] = "" else: - mod["mod_rang"] = rg[0][etudid] - mod["mod_eff"] = rg[1] # effectif dans ce module - mod["mod_rang_txt"] = "%s/%s" % (mod["mod_rang"], mod["mod_eff"]) + if mod_attente: # nt.get_moduleimpls_attente(): + mod["mod_rang"] = scu.RANG_ATTENTE_STR + else: + mod["mod_rang"] = rg[0][etudid] + mod["mod_eff"] = rg[1] # effectif dans ce module + mod["mod_rang_txt"] = "%s/%s" % (mod["mod_rang"], mod["mod_eff"]) else: mod["mod_rang_txt"] = "" if mod_attente: From 2f7e0b06a4d1fc2c69536b82ef0e089f522dd587 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 29 Jan 2022 22:56:01 +0100 Subject: [PATCH 51/70] nouveaux bulletins BUT (commit d712bcf de SL) --- app/static/css/releve-but.css | 620 ++++++++++++++++++---------------- app/static/js/releve-but.js | 119 +++++-- 2 files changed, 404 insertions(+), 335 deletions(-) diff --git a/app/static/css/releve-but.css b/app/static/css/releve-but.css index 3f132d6a..a20c8bfa 100644 --- a/app/static/css/releve-but.css +++ b/app/static/css/releve-but.css @@ -1,298 +1,322 @@ -/* Bulletin BUT, Seb. L. 2021-12-06 */ -/*******************/ -/* Styles généraux */ -/*******************/ -.wait{ - width: 60px; - height: 6px; - margin: auto; - background: #424242; /* la réponse à tout */ - animation: wait .4s infinite alternate; -} -@keyframes wait{ - 100%{transform: translateY(40px) rotate(1turn);} -} -main{ - --couleurPrincipale: rgb(240,250,255); - --couleurFondTitresUE: rgb(206,255,235); - --couleurFondTitresRes: rgb(125, 170, 255); - --couleurFondTitresSAE: rgb(211, 255, 255); - --couleurSecondaire: #fec; - --couleurIntense: #c09; - --couleurSurlignage: rgba(232, 255, 132, 0.47); - max-width: 1000px; - margin: auto; - display: none; -} -.ready .wait{display: none;} -.ready main{display: block;} -h2{ - margin: 0; - color: black; -} -section{ - background: #FFF; - border-radius: 16px; - border: 1px solid #AAA; - padding: 16px 32px; - margin: 8px 0; -} -section>div:nth-child(1){ - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; -} -.CTA_Liste{ - display: flex; - gap: 4px; - align-items: center; - background: var(--couleurIntense); - color: #FFF; - padding: 4px 8px; - border-radius: 4px; - box-shadow: 0 2px 2px rgba(0,0,0,0.26); - cursor: pointer; -} -.CTA_Liste>svg{ - transition: 0.2s; -} -.CTA_Liste:hover{ - outline: 2px solid #424242; -} -.listeOff svg{ - transform: rotate(180deg); -} -.listeOff .syntheseModule, -.listeOff .eval{ - display: none; -} - -.moduleOnOff>.syntheseModule, -.moduleOnOff>.eval{ - display: none; -} -.listeOff .moduleOnOff>.syntheseModule, -.listeOff .moduleOnOff>.eval{ - display: flex !important; -} - -.listeOff .ue::before, -.listeOff .module::before, -.moduleOnOff .ue::before, -.moduleOnOff .module::before{ - transform: rotate(0); -} -.listeOff .moduleOnOff .ue::before, -.listeOff .moduleOnOff .module::before{ - transform: rotate(180deg) !important; -} - -/***********************/ -/* Options d'affichage */ -/***********************/ -.hide_abs .absences, -.hide_abs_modules .module>.absences, -.hide_coef .synthese em, -.hide_coef .eval>em, -.hide_date_inscr .dateInscription, -.hide_ects .ects{ - display: none; -} - -.module>.absences, -.module .moyenne, -.module .info{ - display: none; -} - -/************/ -/* Etudiant */ -/************/ -.info_etudiant{ - color: #000; - text-decoration: none; -} -.etudiant{ - display: flex; - align-items: center; - gap: 16px; - border-color: var(--couleurPrincipale); - background: var(--couleurPrincipale); - color: rgb(0, 0, 0); -} -.civilite{ - font-weight: bold; - font-size: 130%; -} - -/************/ -/* Semestre */ -/************/ -.flex{ - display: flex; - gap: 16px; -} -.infoSemestre{ - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 4px; - flex: none; -} -.infoSemestre>div{ - border: 1px solid var(--couleurIntense); - padding: 4px 8px; - border-radius: 4px; - display: grid; - grid-template-columns: auto auto; - column-gap: 4px; -} -.infoSemestre>div:nth-child(1){ - margin-right: auto; -} -.infoSemestre>div>div:nth-child(even){ - text-align: right; -} -.rang{ - text-decoration: underline var(--couleurIntense); -} -.decision{ - margin: 5px 0; - font-weight: bold; - font-size: 20px; - text-decoration: underline var(--couleurIntense); -} -.enteteSemestre{ - color: black; - font-weight: bold; - font-size: 20px; - margin-bottom: 4px; -} - -/***************/ -/* Synthèse */ -/***************/ -.synthese .ue, -.synthese h3{ - background: var(--couleurFondTitresUE); -} -.synthese em, -.eval em{ - opacity: 0.6; - min-width: 80px; - display: inline-block; -} - -/***************/ -/* Evaluations */ -/***************/ -.module, .ue { - background: var(--couleurSecondaire); - color: #000; - padding: 4px 32px; - border-radius: 4px; - display: flex; - gap: 16px; - margin: 4px 0 2px 0; - overflow-x: auto; - overflow-y: hidden; - cursor: pointer; - position: relative; -} -.module::before, .ue::before { - content:url("data:image/svg+xml;utf8,"); - width: 26px; - height: 26px; - position: absolute; - bottom: 0; - left: 50%; - margin-left: -13px; - transform: rotate(180deg); - transition: 0.2s; -} -h3{ - display: flex; - align-items: center; - margin: 0 auto 0 0; - position: sticky; - left: -32px; - z-index: 1; - font-size: 16px; - background: var(--couleurSecondaire); -} -.sae .module, .sae h3{ - background: var(--couleurFondTitresSAE); -} - -.moyenne{ - font-weight: bold; - text-align: right; -} -.info{ - opacity: 0.9; -} -.syntheseModule{ - cursor: pointer; -} -.eval, .syntheseModule{ - position: relative; - display: flex; - justify-content: space-between; - margin: 0 0 0 28px; - padding: 0px 4px; - border-bottom: 1px solid #aaa; -} -.eval>div, .syntheseModule>div{ - display: flex; - gap: 4px; -} - -.eval:hover, .syntheseModule:hover{ - background: var(--couleurSurlignage); - /* color: #FFF; */ -} -.complement{ - pointer-events:none; - position: absolute; - bottom: 100%; - right: 0; - padding: 8px; - border-radius: 4px; - background: #FFF; - color: #000; - border: 1px solid var(--couleurIntense); - opacity: 0; - display: grid !important; - grid-template-columns: auto auto; - gap: 0 !important; - column-gap: 4px !important; -} -.eval:hover .complement{ - opacity: 1; - z-index: 1; -} -.complement>div:nth-child(even){ - text-align: right; -} -.complement>div:nth-child(1), -.complement>div:nth-child(2){ - font-weight: bold; -} -.complement>div:nth-child(1), -.complement>div:nth-child(7){ - margin-bottom: 8px; -} - -.absences{ - display: grid; - grid-template-columns: auto auto; - column-gap: 4px; - text-align: right; - border-left: 1px solid; - padding-left: 16px; -} -.absences>div:nth-child(1), -.absences>div:nth-child(2){ - font-weight: bold; -} \ No newline at end of file +/* Bulletin BUT, Seb. L. 2021-12-06 */ +/*******************/ +/* Styles généraux */ +/*******************/ +.wait{ + width: 60px; + height: 6px; + margin: auto; + background: #424242; /* la réponse à tout */ + animation: wait .4s infinite alternate; +} +@keyframes wait{ + 100%{transform: translateY(40px) rotate(1turn);} +} +main{ + --couleurPrincipale: rgb(240,250,255); + --couleurFondTitresUE: rgb(206,255,235); + --couleurFondTitresRes: rgb(125, 170, 255); + --couleurFondTitresSAE: rgb(211, 255, 255); + --couleurSecondaire: #fec; + --couleurIntense: #c09; + --couleurSurlignage: rgba(232, 255, 132, 0.47); + max-width: 1000px; + margin: auto; + display: none; +} +.ready .wait{display: none;} +.ready main{display: block;} +h2{ + margin: 0; + color: black; +} +section{ + background: #FFF; + border-radius: 16px; + border: 1px solid #AAA; + padding: 16px 32px; + margin: 8px 0; +} +section>div:nth-child(1){ + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} +.CTA_Liste{ + display: flex; + gap: 4px; + align-items: center; + background: var(--couleurIntense); + color: #FFF; + padding: 4px 8px; + border-radius: 4px; + box-shadow: 0 2px 2px rgba(0,0,0,0.26); + cursor: pointer; +} +.CTA_Liste>svg{ + transition: 0.2s; +} +.CTA_Liste:hover{ + outline: 2px solid #424242; +} +.listeOff svg{ + transform: rotate(180deg); +} +.listeOff .syntheseModule, +.listeOff .eval{ + display: none; +} + +.moduleOnOff>.syntheseModule, +.moduleOnOff>.eval{ + display: none; +} +.listeOff .moduleOnOff>.syntheseModule, +.listeOff .moduleOnOff>.eval{ + display: flex !important; +} + +.listeOff .ue::before, +.listeOff .module::before, +.moduleOnOff .ue::before, +.moduleOnOff .module::before{ + transform: rotate(0); +} +.listeOff .moduleOnOff .ue::before, +.listeOff .moduleOnOff .module::before{ + transform: rotate(180deg) !important; +} + +/***********************/ +/* Options d'affichage */ +/***********************/ +.hide_abs .absencesRecap, +/*.hide_abs .absences,*/ +.hide_abs_modules .module>.absences, +.hide_coef .synthese em, +.hide_coef .eval>em, +.hide_date_inscr .dateInscription, +.hide_ects .ects{ + display: none; +} + +/*.module>.absences,*/ +.module .moyenne, +.module .info{ + display: none; +} + +/************/ +/* Etudiant */ +/************/ +.info_etudiant{ + color: #000; + text-decoration: none; +} +.etudiant{ + display: flex; + align-items: center; + gap: 16px; + border-color: var(--couleurPrincipale); + background: var(--couleurPrincipale); + color: rgb(0, 0, 0); +} +.civilite{ + font-weight: bold; + font-size: 130%; +} + +/************/ +/* Semestre */ +/************/ +.flex{ + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} +.infoSemestre{ + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 4px; +} +.infoSemestre>div{ + border: 1px solid var(--couleurIntense); + padding: 4px 8px; + border-radius: 4px; + display: grid; + grid-template-columns: auto auto; + column-gap: 4px; + flex: none; +} +.infoSemestre>div:nth-child(1){ + margin-right: auto; +} +.infoSemestre>div>div:nth-child(even){ + text-align: right; +} +.rang{ + text-decoration: underline var(--couleurIntense); +} +.decision{ + margin: 5px 0; + font-weight: bold; + font-size: 20px; + text-decoration: underline var(--couleurIntense); +} +.enteteSemestre{ + color: black; + font-weight: bold; + font-size: 20px; + margin-bottom: 4px; +} +/***************/ +/* Zone custom */ +/***************/ +.custom:empty{ + display: none; +} + +/***************/ +/* Synthèse */ +/***************/ +.synthese .ue, +.synthese h3{ + background: var(--couleurFondTitresUE); +} +.synthese em, +.eval em{ + opacity: 0.6; + min-width: 80px; + display: inline-block; +} +.ueBonus, +.ueBonus h3{ + background: var(--couleurFondTitresSAE) !important; + color: #000 !important; +} + +/***************/ +/* Evaluations */ +/***************/ +.evaluations>div, +.sae>div{ + scroll-margin-top: 60px; +} +.module, .ue { + background: var(--couleurSecondaire); + color: #000; + padding: 4px 32px; + border-radius: 4px; + display: flex; + gap: 16px; + margin: 4px 0 2px 0; + overflow-x: auto; + overflow-y: hidden; + cursor: pointer; + position: relative; +} +.module::before, .ue::before { + content:url("data:image/svg+xml;utf8,"); + width: 26px; + height: 26px; + position: absolute; + bottom: 0; + left: calc(50% - 13px); + transform: rotate(180deg); + transition: 0.2s; +} +@media screen and (max-width: 1000px) { + /* Placer le chevron à gauche au lieu du milieu */ + .module::before, .ue::before { + left: 2px; + bottom: calc(50% - 13px); + } +} +h3{ + display: flex; + align-items: center; + margin: 0 auto 0 0; + position: sticky; + left: -32px; + z-index: 1; + font-size: 16px; + background: var(--couleurSecondaire); +} +.sae .module, .sae h3{ + background: var(--couleurFondTitresSAE); +} + +.moyenne{ + font-weight: bold; + text-align: right; +} +.info{ + opacity: 0.9; +} +.syntheseModule{ + cursor: pointer; +} +.eval, .syntheseModule{ + position: relative; + display: flex; + justify-content: space-between; + margin: 0 0 0 28px; + padding: 0px 4px; + border-bottom: 1px solid #aaa; +} +.eval>div, .syntheseModule>div{ + display: flex; + gap: 4px; +} + +.eval:hover, .syntheseModule:hover{ + background: var(--couleurSurlignage); + /* color: #FFF; */ +} +.complement{ + pointer-events:none; + position: absolute; + bottom: 100%; + right: 0; + padding: 8px; + border-radius: 4px; + background: #FFF; + color: #000; + border: 1px solid var(--couleurIntense); + opacity: 0; + display: grid !important; + grid-template-columns: auto auto; + gap: 0 !important; + column-gap: 4px !important; +} +.eval:hover .complement{ + opacity: 1; + z-index: 1; +} +.complement>div:nth-child(even){ + text-align: right; +} +.complement>div:nth-child(1), +.complement>div:nth-child(2){ + font-weight: bold; +} +.complement>div:nth-child(1), +.complement>div:nth-child(7){ + margin-bottom: 8px; +} + +/*.absences{ + display: grid; + grid-template-columns: auto auto; + column-gap: 4px; + text-align: right; + border-left: 1px solid; + padding-left: 16px; +} +.absences>div:nth-child(1), +.absences>div:nth-child(2){ + font-weight: bold; +}*/ diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index 521e97da..6e18ad57 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -15,13 +15,10 @@ class releveBUT extends HTMLElement { /* Style du module */ const styles = document.createElement('link'); styles.setAttribute('rel', 'stylesheet'); - styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css'); - /* variante "ScoDoc" ou "Passerelle" (ENT) ? */ - if (location.href.split("/")[3] == "ScoDoc") { /* un peu osé... */ - styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css'); + if (location.href.split("/")[3] == "ScoDoc") { + styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css'); // Scodoc } else { - // Passerelle - styles.setAttribute('href', '/assets/styles/releve-but.css'); + styles.setAttribute('href', '/assets/styles/releve-but.css'); // Passerelle } this.shadow.appendChild(styles); } @@ -49,6 +46,8 @@ class releveBUT extends HTMLElement { this.showSynthese(data); this.showEvaluations(data); + this.showCustom(data); + this.setOptions(data.options); this.shadow.querySelectorAll(".CTA_Liste").forEach(e => { @@ -57,7 +56,7 @@ class releveBUT extends HTMLElement { this.shadow.querySelectorAll(".ue, .module").forEach(e => { e.addEventListener("click", this.moduleOnOff) }) - this.shadow.querySelectorAll(".syntheseModule").forEach(e => { + this.shadow.querySelectorAll(":not(.ueBonus)+.syntheseModule").forEach(e => { e.addEventListener("click", this.goTo) }) @@ -77,6 +76,11 @@ class releveBUT extends HTMLElement {
+ + + +
+ @@ -169,8 +173,8 @@ class releveBUT extends HTMLElement { output += `
- Numéro étudiant : ${data.etudiant.code_nip} - - Code INE : ${data.etudiant.code_ine} + Numéro étudiant : ${data.etudiant.code_nip || "~"} - + Code INE : ${data.etudiant.code_ine || "~"}
${data.formation.titre}
`; @@ -183,6 +187,13 @@ class releveBUT extends HTMLElement { this.shadow.querySelector(".infoEtudiant").innerHTML = output; } + /*******************************/ + /* Affichage local */ + /*******************************/ + showCustom(data) { + this.shadow.querySelector(".custom").innerHTML = data.custom || ""; + } + /*******************************/ /* Information sur le semestre */ /*******************************/ @@ -196,6 +207,11 @@ class releveBUT extends HTMLElement {
Max. promo. :
${data.semestre.notes.max}
Moy. promo. :
${data.semestre.notes.moy}
Min. promo. :
${data.semestre.notes.min}
+ +
+
Absences
+
N.J. ${data.semestre.absences?.injustifie ?? "-"}
+
Total ${data.semestre.absences?.total ?? "-"}
`; /*${data.semestre.groupes.map(groupe => { return ` @@ -210,7 +226,7 @@ class releveBUT extends HTMLElement { }).join("") }*/ this.shadow.querySelector(".infoSemestre").innerHTML = output; - /*this.shadow.querySelector(".decision").innerHTML = data.semestre.decision.code;*/ + this.shadow.querySelector(".decision").innerHTML = data.semestre.decision?.code || ""; } /*******************************/ @@ -219,32 +235,44 @@ class releveBUT extends HTMLElement { showSynthese(data) { let output = ``; Object.entries(data.ues).forEach(([ue, dataUE]) => { - output += ` - -
-
-

- ${(dataUE.competence) ? dataUE.competence + " - " : ""}${ue} -

-
-
Moyenne : ${dataUE.moyenne?.value || "-"}
-
- Bonus : ${dataUE.bonus || 0} - - Malus : ${dataUE.malus || 0} -  - - ECTS : ${dataUE.ECTS.acquis} / ${dataUE.ECTS.total} - -
-
-
-
Abs N.J.
${dataUE.absences?.injustifie || 0}
-
Total
${dataUE.absences?.total || 0}
+ if (dataUE.type == 1) { // UE Sport / Bonus + output += ` +
+
+

Bonus

+
${dataUE.bonus_description}
+ ${this.ueSport(dataUE.modules)}
- ${this.synthese(data, dataUE.ressources)} - ${this.synthese(data, dataUE.saes)} -
- `; + `; + } else { + output += ` +
+
+

+ ${ue}${(dataUE.competence) ? " - " + dataUE.competence : ""} +

+
+
Moyenne : ${dataUE.moyenne?.value || "-"}
+
+ Bonus : ${dataUE.bonus || 0} - + Malus : ${dataUE.malus || 0} +  - + ECTS : ${dataUE.ECTS.acquis} / ${dataUE.ECTS.total} + +
+
`; + /*
+
Abs N.J.
${dataUE.absences?.injustifie || 0}
+
Total
${dataUE.absences?.total || 0}
+
*/ + output += ` +
+ ${this.synthese(data, dataUE.ressources)} + ${this.synthese(data, dataUE.saes)} +
+ `; + } }); this.shadow.querySelector(".synthese").innerHTML = output; } @@ -252,7 +280,7 @@ class releveBUT extends HTMLElement { let output = ""; Object.entries(modules).forEach(([module, dataModule]) => { let titre = data.ressources[module]?.titre || data.saes[module]?.titre; - let url = data.ressources[module]?.url || data.saes[module]?.url; + //let url = data.ressources[module]?.url || data.saes[module]?.url; output += `
${module} - ${titre}
@@ -265,6 +293,23 @@ class releveBUT extends HTMLElement { }) return output; } + ueSport(modules) { + let output = ""; + Object.values(modules).forEach((module) => { + Object.values(module.evaluations).forEach((evaluation) => { + output += ` +
+
${module.titre} - ${evaluation.description}
+
+ ${evaluation.note.value ?? "-"} + Coef. ${evaluation.coef} +
+
+ `; + }) + }) + return output; + } /*******************************/ /* Evaluations */ @@ -305,7 +350,7 @@ class releveBUT extends HTMLElement { evaluations.forEach((evaluation) => { output += `
-
${this.URL(evaluation.url, evaluation.description)}
+
${this.URL(evaluation.url, evaluation.description || "Évaluation")}
${evaluation.note.value} Coef. ${evaluation.coef} @@ -363,4 +408,4 @@ class releveBUT extends HTMLElement { } } -customElements.define('releve-but', releveBUT); \ No newline at end of file +customElements.define('releve-but', releveBUT); From 41469707af3ee3b096c5b8b2a5e3f78249f7282c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 29 Jan 2022 22:59:40 +0100 Subject: [PATCH 52/70] Ajout titre d'UE au bulletin BUT JSON --- app/but/bulletin_but.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index b9e33478..14e9429d 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -57,6 +57,7 @@ class BulletinBUT(ResultatsSemestreBUT): "dict synthèse résultats UE" d = { "id": ue.id, + "titre": ue.titre, "numero": ue.numero, "type": ue.type, "ECTS": { From 920aeb066ffbb161a13fe75bd19d61f96262d19e Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 29 Jan 2022 23:36:07 +0100 Subject: [PATCH 53/70] =?UTF-8?q?Bul.=20BUT:=20n'affiche=20que=20les=20UE?= =?UTF-8?q?=20dans=20lesquelles=20on=20est=20inscrit=20=C3=A0=20au=20moins?= =?UTF-8?q?=20un=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/bulletin_but.py | 14 ++++++++++---- app/comp/inscr_mod.py | 2 ++ app/comp/res_common.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 14e9429d..cc51e2a2 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -32,7 +32,7 @@ class BulletinBUT(ResultatsSemestreBUT): ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id) etud_moy_module = self.sem_cube[etud_idx] # module x UE for modimpl in modimpls: - if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit + if self.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit if ue.type != UE_SPORT: coef = self.modimpl_coefs_df[modimpl.id][ue.id] if coef > 0: @@ -115,7 +115,7 @@ class BulletinBUT(ResultatsSemestreBUT): # except RuntimeWarning: # all nans in np.nanmean # pass modimpl_results = self.modimpls_results[modimpl.id] - if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit + if self.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit d[modimpl.module.code] = { "id": modimpl.id, "titre": modimpl.module.titre, @@ -183,7 +183,9 @@ class BulletinBUT(ResultatsSemestreBUT): details = [ f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}" for ue in self.ues - if ue.id in self.bonus_ues and bonus_vect[ue.id] > 0.0 + if self.modimpls_in_ue(ue.id, etudid) + and ue.id in self.bonus_ues + and bonus_vect[ue.id] > 0.0 ] if details: return "Bonus de " + ", ".join(details) @@ -256,7 +258,11 @@ class BulletinBUT(ResultatsSemestreBUT): "ressources": self.etud_mods_results(etud, self.ressources), "saes": self.etud_mods_results(etud, self.saes), "ues": { - ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues + ue.acronyme: self.etud_ue_results(etud, ue) + for ue in self.ues + if self.modimpls_in_ue( + ue.id, etud.id + ) # si l'UE comporte des modules auxquels on est inscrit }, "semestre": semestre_infos, }, diff --git a/app/comp/inscr_mod.py b/app/comp/inscr_mod.py index b9be1e9f..667eacbd 100644 --- a/app/comp/inscr_mod.py +++ b/app/comp/inscr_mod.py @@ -35,6 +35,8 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame: dtype=int, ) df = df.merge(ins_df, how="left", left_index=True, right_index=True) + # Force columns names to integers (moduleimpl ids) + df.columns = pd.Int64Index([int(x) for x in df.columns], dtype="int") # les colonnes de df sont en float (Nan) quand il n'y a # aucun inscrit au module. df.fillna(0, inplace=True) # les non-inscrits diff --git a/app/comp/res_common.py b/app/comp/res_common.py index a90ce1c1..7bc2bff2 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -130,6 +130,16 @@ class ResultatsSemestre: """ return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all() + def modimpls_in_ue(self, ue_id, etudid): + """Liste des modimpl de cet ue auxquels l'étudiant est inscrit""" + # sert pour l'affichage ou non de l'UE sur le bulletin + return [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if modimpl.module.ue.id == ue_id + and self.modimpl_inscr_df[modimpl.id][etudid] + ] + @cached_property def ue_au_dessus(self, seuil=10.0) -> pd.DataFrame: """DataFrame columns UE, rows etudid, valeurs: bool From d928a6be32e91946912938f28d693c106109d09a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 08:13:24 +0100 Subject: [PATCH 54/70] =?UTF-8?q?BUT:=20modif=20semestre:=20modules=20de?= =?UTF-8?q?=20m=C3=AAme=20semestre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_formsemestre_edit.py | 159 ++++++++++++++++++---------- 1 file changed, 105 insertions(+), 54 deletions(-) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index e6ac86e3..b1f6faa6 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -28,13 +28,16 @@ """Form choix modules / responsables et creation formsemestre """ import flask -from flask import url_for, g, request +from flask import url_for, flash +from flask import g, request from flask_login import current_user from app import db from app.auth.models import User from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN -from app.models import ModuleImpl, Evaluation, EvaluationUEPoids +from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids +from app.models.formations import Formation +from app.models.formsemestre import FormSemestre import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc import sco_cache @@ -65,9 +68,9 @@ from app.scodoc import sco_preferences from app.scodoc import sco_users -def _default_sem_title(F): - """Default title for a semestre in formation F""" - return F["titre"] +def _default_sem_title(formation): + """Default title for a semestre in formation""" + return formation.titre def formsemestre_createwithmodules(): @@ -140,6 +143,7 @@ def do_formsemestre_createwithmodules(edit=False): if edit: formsemestre_id = int(vals["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(formsemestre_id) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if not current_user.has_permission(Permission.ScoImplement): if not edit: # il faut ScoImplement pour creer un semestre @@ -161,26 +165,25 @@ def do_formsemestre_createwithmodules(edit=False): allowed_user_names = list(uid2display.values()) + [""] # formation_id = int(vals["formation_id"]) - F = sco_formations.formation_list(args={"formation_id": formation_id}) - if not F: + formation = Formation.query.get(formation_id) + if formation is None: raise ScoValueError("Formation inexistante !") - F = F[0] if not edit: - initvalues = {"titre": _default_sem_title(F)} + initvalues = {"titre": _default_sem_title(formation)} semestre_id = int(vals["semestre_id"]) - sem_module_ids = set() + module_ids_set = set() else: # setup form init values initvalues = sem semestre_id = initvalues["semestre_id"] # add associated modules to tf-checked: - ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) - sem_module_ids = set([x["module_id"] for x in ams]) - initvalues["tf-checked"] = ["MI" + str(x["module_id"]) for x in ams] - for x in ams: - initvalues["MI" + str(x["module_id"])] = uid2display.get( - x["responsable_id"], - f"inconnu numéro {x['responsable_id']} resp. de {x['moduleimpl_id']} !", + module_ids_existing = [modimpl.module.id for modimpl in formsemestre.modimpls] + module_ids_set = set(module_ids_existing) + initvalues["tf-checked"] = ["MI" + str(x) for x in module_ids_existing] + for modimpl in formsemestre.modimpls: + initvalues[f"MI{modimpl.module.id}"] = uid2display.get( + modimpl.responsable_id, + f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !", ) initvalues["responsable_id"] = uid2display.get( @@ -192,15 +195,20 @@ def do_formsemestre_createwithmodules(edit=False): ) # Liste des ID de semestres - if F["type_parcours"] is not None: - parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) + if formation.type_parcours is not None: + parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours) NB_SEM = parcours.NB_SEM else: NB_SEM = 10 # fallback, max 10 semestres if NB_SEM == 1: semestre_id_list = [-1] else: - semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) + if edit and formation.is_apc(): + # en APC, ne permet pas de changer de semestre + semestre_id_list = [formsemestre.semestre_id] + else: + semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) + semestre_id_labels = [] for sid in semestre_id_list: if sid == -1: @@ -319,7 +327,7 @@ def do_formsemestre_createwithmodules(edit=False): "explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans le titre: ils seront automatiquement ajoutés """ - % _default_sem_title(F), + % _default_sem_title(formation), }, ), ( @@ -340,6 +348,9 @@ def do_formsemestre_createwithmodules(edit=False): "title": "Semestre dans la formation", "allowed_values": semestre_id_list, "labels": semestre_id_labels, + "explanation": "en BUT, on ne peut pas modifier le semestre après création" + if formation.is_apc() + else "", }, ), ) @@ -549,7 +560,12 @@ def do_formsemestre_createwithmodules(edit=False): ) ) for mod in mods: - if mod["semestre_id"] == semestre_id: + if mod["semestre_id"] == semestre_id and ( + (not edit) # creation => tous modules + or (not formation.is_apc()) # pas BUT, on peux mixer les semestres + or (semestre_id == formsemestre.semestre_id) # module du semestre + or (mod["module_id"] in module_ids_set) # module déjà présent + ): nbmod += 1 if edit: select_name = "%s!group_id" % mod["module_id"] @@ -560,7 +576,7 @@ def do_formsemestre_createwithmodules(edit=False): else: return "" - if mod["module_id"] in sem_module_ids: + if mod["module_id"] in module_ids_set: disabled = "disabled" else: disabled = "" @@ -684,12 +700,13 @@ def do_formsemestre_createwithmodules(edit=False): msg = '
  • Code étape Apogée manquant
' if tf[0] == 0 or msg: - return ( - '

Formation %(titre)s (%(acronyme)s), version %(version)s, code %(formation_code)s

' - % F - + msg - + str(tf[1]) - ) + return f"""

Formation {formation.titre} ({formation.acronyme}), version {formation.version}, code {formation.formation_code} +

+ {msg} + {tf[1]} + """ elif tf[0] == -1: return "

annulation

" else: @@ -735,42 +752,58 @@ def do_formsemestre_createwithmodules(edit=False): etape=tf[2]["etape_apo" + str(n)], vdi=tf[2]["vdi_apo" + str(n)] ) ) + # Modules sélectionnés: + # (retire le "MI" du début du nom de champs) + module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]] if not edit: - # creation du semestre + if formation.is_apc(): + _formsemestre_check_module_list( + module_ids_checked, tf[2]["semestre_id"] + ) + # création du semestre formsemestre_id = sco_formsemestre.do_formsemestre_create(tf[2]) - # creation des modules - for module_id in tf[2]["tf-checked"]: - assert module_id[:2] == "MI" + # création des modules + for module_id in module_ids_checked: modargs = { - "module_id": int(module_id[2:]), + "module_id": module_id, "formsemestre_id": formsemestre_id, - "responsable_id": tf[2][module_id], + "responsable_id": tf[2][f"MI{module_id}"], } _ = sco_moduleimpl.do_moduleimpl_create(modargs) + flash("Nouveau semestre créé") return flask.redirect( - "formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé" - % formsemestre_id + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) ) else: - # modification du semestre: + # Modification du semestre: # on doit creer les modules nouvellement selectionnés - # modifier ceux a modifier, et DETRUIRE ceux qui ne sont plus selectionnés. - # Note: la destruction echouera s'il y a des objets dependants - # (eg des evaluations définies) - # nouveaux modules - # (retire le "MI" du début du nom de champs) - checkedmods = [int(x[2:]) for x in tf[2]["tf-checked"]] + # modifier ceux à modifier, et DETRUIRE ceux qui ne sont plus selectionnés. + # Note: la destruction échouera s'il y a des objets dépendants + # (eg des évaluations définies) + module_ids_tocreate = [ + x for x in module_ids_checked if not x in module_ids_existing + ] + if formation.is_apc(): + _formsemestre_check_module_list( + module_ids_tocreate, tf[2]["semestre_id"] + ) + # modules existants à modifier + module_ids_toedit = [ + x for x in module_ids_checked if x in module_ids_existing + ] + # modules à détruire + module_ids_todelete = [ + x for x in module_ids_existing if not x in module_ids_checked + ] + # sco_formsemestre.do_formsemestre_edit(tf[2]) - ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id) - existingmods = [x["module_id"] for x in ams] - mods_tocreate = [x for x in checkedmods if not x in existingmods] - # modules a existants a modifier - mods_toedit = [x for x in checkedmods if x in existingmods] - # modules a detruire - mods_todelete = [x for x in existingmods if not x in checkedmods] # msg = [] - for module_id in mods_tocreate: + for module_id in module_ids_tocreate: modargs = { "module_id": module_id, "formsemestre_id": formsemestre_id, @@ -808,9 +841,11 @@ def do_formsemestre_createwithmodules(edit=False): % (module_id, moduleimpl_id) ) # - ok, diag = formsemestre_delete_moduleimpls(formsemestre_id, mods_todelete) + ok, diag = formsemestre_delete_moduleimpls( + formsemestre_id, module_ids_todelete + ) msg += diag - for module_id in mods_toedit: + for module_id in module_ids_toedit: moduleimpl_id = sco_moduleimpl.moduleimpl_list( formsemestre_id=formsemestre_id, module_id=module_id )[0]["moduleimpl_id"] @@ -847,6 +882,22 @@ def do_formsemestre_createwithmodules(edit=False): ) +def _formsemestre_check_module_list(module_ids, semestre_idx): + """En APC: Vérifie que tous les modules de la liste + sont dans le semestre indiqué. + Sinon, raise ScoValueError. + """ + # vérification de la cohérence / modules / semestre + mod_sems_idx = { + Module.query.get_or_404(module_id).ue.semestre_idx for module_id in module_ids + } + if mod_sems_idx and mod_sems_idx != {semestre_idx}: + raise ScoValueError( + "Les modules sélectionnés ne sont pas tous dans le semestre choisi !", + dest_url="javascript:history.back();", + ) + + def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del): """Delete moduleimpls module_ids_to_del: list of module_id (warning: not moduleimpl) From bfda20d5a61fb08b75dcd264565b82da3dd3ff24 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 08:17:25 +0100 Subject: [PATCH 55/70] BUT: pas de coef d'UE sur le formulaire --- app/scodoc/sco_edit_ue.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 5c4741f7..5b333a9f 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -333,7 +333,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None): la moyenne générale est activée. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. + Jamais utilisé en BUT. """, + "enabled": not is_apc, }, ), ( From 05139cfcf466c3c4172273a38c8be0cbf94b857a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 08:25:22 +0100 Subject: [PATCH 56/70] closes #297 --- app/scodoc/sco_edit_module.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 8359d12b..4053bde0 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -712,15 +712,17 @@ def module_edit(module_id=None): ) ) else: - # l'UE peut changer + # l'UE de rattachement peut changer tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") old_ue_id = a_module.ue.id new_ue_id = int(tf[2]["ue_id"]) if (old_ue_id != new_ue_id) and in_use: - # pas changer de semestre un module utilisé ! - raise ScoValueError( - "Module utilisé: il ne peut pas être changé de semestre !" - ) + new_ue = UniteEns.query.get_or_404(new_ue_id) + if new_ue.semestre_idx != a_module.ue.semestre_idx: + # pas changer de semestre un module utilisé ! + raise ScoValueError( + "Module utilisé: il ne peut pas être changé de semestre !" + ) # En APC, force le semestre égal à celui de l'UE if is_apc: selected_ue = UniteEns.query.get(tf[2]["ue_id"]) From e060be1b3da7541f9b7fe275508863831adb303b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 09:05:51 +0100 Subject: [PATCH 57/70] Close #282 --- app/scodoc/sco_edit_module.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 4053bde0..1c42d53f 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -700,8 +700,28 @@ def module_edit(module_id=None): initvalues=module, submitlabel="Modifier ce module", ) + # Affiche liste des formseemstre utilisant ce module + if in_use: + formsemestre_ids = {modimpl.formsemestre_id for modimpl in a_module.modimpls} + formsemestres = [FormSemestre.query.get(fid) for fid in formsemestre_ids] + formsemestres.sort(key=lambda f: f.date_debut) + items = [ + f"""{f.titre}""" + for f in formsemestres + ] + sem_descr = f""" +
+
Ce module est utilisé dans les formsemestres suivants:
+
  • + {"
  • ".join( items )} +
+
+ """ + # if tf[0] == 0: - return "\n".join(H) + tf[1] + html_sco_header.sco_footer() + return "\n".join(H) + tf[1] + sem_descr + html_sco_header.sco_footer() elif tf[0] == -1: return flask.redirect( url_for( From d17a2238e46c0e4cee1cd71abe44dccdea6dcad6 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 09:29:51 +0100 Subject: [PATCH 58/70] BUT: affiche titre UE sur bulletin html --- app/static/js/releve-but.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index 6e18ad57..aceec68c 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -250,7 +250,7 @@ class releveBUT extends HTMLElement {

- ${ue}${(dataUE.competence) ? " - " + dataUE.competence : ""} + ${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}

Moyenne : ${dataUE.moyenne?.value || "-"}
From 514623e8cf239b62f39c933b8d4dceb5c51de152 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 09:37:06 +0100 Subject: [PATCH 59/70] Bonus: clip > 0 cas DUT/no max --- app/comp/bonus_spo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index e9f615cf..f8e82e41 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -211,6 +211,8 @@ class BonusSportAdditif(BonusSport): bonus_moy_arr = np.clip( bonus_moy_arr, 0.0, self.bonus_max, out=bonus_moy_arr ) + else: # necessaire pour éviter bonus négatifs ! + bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr) # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus) if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues: From 3ef1f155b7170a5d1a1e569c4fd0921091b3bc80 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 11:56:55 +0100 Subject: [PATCH 60/70] =?UTF-8?q?BUT:=20cr=C3=A9ation=20module:=20choix=20?= =?UTF-8?q?semestre=20et=20non=20plus=20UE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_edit_apc.py | 9 ++++++++ app/scodoc/sco_edit_module.py | 41 ++++----------------------------- app/templates/pn/form_mods.html | 4 ++-- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index a9eff6de..c1c75319 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -33,6 +33,7 @@ from flask_login import current_user from app import db from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl from app.models.notes import ScolarFormSemestreValidation +from app.scodoc.sco_codes_parcours import UE_SPORT import app.scodoc.sco_utils as scu from app.scodoc import sco_groups from app.scodoc.sco_utils import ModuleType @@ -99,12 +100,19 @@ def html_edit_formation_apc( ressources_in_sem = ressources.filter_by(semestre_id=semestre_idx) saes_in_sem = saes.filter_by(semestre_id=semestre_idx) other_modules_in_sem = other_modules.filter_by(semestre_id=semestre_idx) + matiere_parent = Matiere.query.filter( + Matiere.ue_id == UniteEns.id, + UniteEns.formation_id == formation.id, + UniteEns.semestre_idx == semestre_idx, + UniteEns.type != UE_SPORT, + ).first() H += [ render_template( "pn/form_mods.html", formation=formation, titre=f"Ressources du S{semestre_idx}", create_element_msg="créer une nouvelle ressource", + matiere_parent=matiere_parent, modules=ressources_in_sem, module_type=ModuleType.RESSOURCE, editable=editable, @@ -117,6 +125,7 @@ def html_edit_formation_apc( formation=formation, titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", create_element_msg="créer une nouvelle SAÉ", + matiere_parent=matiere_parent, modules=saes_in_sem, module_type=ModuleType.SAE, editable=editable, diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 1c42d53f..ae143cbc 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -145,7 +145,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): ] if is_apc: H += [ - f"""

Création {object_name} dans la formation {ue.formation.acronyme}

""" + f"""

Création {object_name} dans la formation {ue.formation.acronyme}, Semestre {ue.semestre_idx}, {ue.acronyme}

""" ] else: H += [ @@ -191,35 +191,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): ), ] semestres_indices = list(range(1, parcours.NB_SEM + 1)) - if is_apc: # BUT: choix de l'UE de rattachement (qui donnera le semestre) - descr += [ - ( - "ue_id", - { - "input_type": "menu", - "type": "int", - "title": "UE de rattachement", - "explanation": "utilisée pour la présentation dans certains documents", - "labels": [f"{u.acronyme} {u.titre}" for u in ues], - "allowed_values": [u.id for u in ues], - }, - ), - ] - else: - # Formations classiques: choix du semestre - descr += [ - ( - "semestre_id", - { - "input_type": "menu", - "type": "int", - "title": parcours.SESSION_NAME.capitalize(), - "explanation": "%s du module" % parcours.SESSION_NAME, - "labels": [str(x) for x in semestres_indices], - "allowed_values": semestres_indices, - }, - ), - ] + descr += [ ( "module_type", @@ -318,12 +290,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() else: - if is_apc: - # BUT: l'UE indique le semestre - selected_ue = UniteEns.query.get(tf[2]["ue_id"]) - if selected_ue is None: - raise ValueError("UE invalide") - tf[2]["semestre_id"] = selected_ue.semestre_idx + tf[2]["semestre_id"] = ue.semestre_idx _ = do_module_create(tf[2]) @@ -719,6 +686,8 @@ def module_edit(module_id=None):
""" + else: + sem_descr = "" # if tf[0] == 0: return "\n".join(H) + tf[1] + sem_descr + html_sco_header.sco_footer() diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html index cba28347..e5ca79e5 100644 --- a/app/templates/pn/form_mods.html +++ b/app/templates/pn/form_mods.html @@ -71,12 +71,12 @@ {% endfor %} - {% if editable and formation.ues.count() and formation.ues[0].matieres.count() %} + {% if editable and matiere_parent %}
  • {{create_element_msg}}
  • From 8bccdd3ae24f22204cae45f6857d219e4eb54455 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 13:11:17 +0100 Subject: [PATCH 61/70] =?UTF-8?q?Fix:=20evaluation=20completes=20en=20pr?= =?UTF-8?q?=C3=A9sence=20de=20DEM=20ou=20DEF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/moy_mod.py | 14 +++++++------- app/models/formsemestre.py | 5 +++++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 1a64809f..bf4afe7a 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -66,7 +66,7 @@ class ModuleImplResults: self.moduleimpl_id = moduleimpl.id self.module_id = moduleimpl.module.id self.etudids = None - "liste des étudiants inscrits au SEMESTRE" + "liste des étudiants inscrits au SEMESTRE (incluant dem et def)" self.nb_inscrits_module = None "nombre d'inscrits (non DEM) à ce module" @@ -120,7 +120,7 @@ class ModuleImplResults: # --- Calcul nombre d'inscrits pour déterminer les évaluations "completes": # on prend les inscrits au module ET au semestre (donc sans démissionnaires) inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection( - self.etudids + moduleimpl.formsemestre.etudids_actifs ) self.nb_inscrits_module = len(inscrits_module) @@ -128,14 +128,14 @@ class ModuleImplResults: evals_notes = pd.DataFrame(index=self.etudids, dtype=float) self.evaluations_completes = [] self.evaluations_completes_dict = {} + for evaluation in moduleimpl.evaluations: eval_df = self._load_evaluation_notes(evaluation) # is_complete ssi tous les inscrits (non dem) au semestre ont une note - # ou évaluaton déclarée "à prise en compte immédiate" - is_complete = ( - len(set(eval_df.index).intersection(self.etudids)) - == self.nb_inscrits_module - ) or evaluation.publish_incomplete # immédiate + # ou évaluation déclarée "à prise en compte immédiate" + is_complete = evaluation.publish_incomplete or ( + not (inscrits_module - set(eval_df.index)) + ) self.evaluations_completes.append(is_complete) self.evaluations_completes_dict[evaluation.id] = is_complete diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index a6016031..d059701a 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -313,6 +313,11 @@ class FormSemestre(db.Model): else: return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT] + @cached_property + def etudids_actifs(self) -> set: + "Set des etudids inscrits non démissionnaires" + return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT} + @cached_property def etuds_inscriptions(self) -> dict: """Map { etudid : inscription } (incluant DEM et DEF)""" From ae757a441e4c22a4848fe24229e98f67cd1642be Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 13:33:01 +0100 Subject: [PATCH 62/70] =?UTF-8?q?Bonus=20sport=20Tours=20diff=C3=A9renci?= =?UTF-8?q?=C3=A9=20GEII=20/=20le=20reste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/bonus_spo.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index f8e82e41..12a382af 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -16,6 +16,8 @@ import datetime import numpy as np import pandas as pd +from flask import g + from app.models.formsemestre import FormSemestre from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -559,6 +561,8 @@ class BonusTours(BonusDirect): et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale, soit pour le BUT à chaque moyenne d'UE. + Attention: en GEII, facteur 1/40, ailleurs facteur 1. + Le bonus total est limité à 1 point. """ @@ -568,6 +572,16 @@ class BonusTours(BonusDirect): seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés proportion_point = 1.0 / 40.0 + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul différencié selon le département !""" + if g.scodoc_dept == "GEII": + self.proportion_point = 1.0 / 40.0 + else: + self.proportion_point = 1.0 + return super().compute_bonus( + self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) + class BonusVilleAvray(BonusSport): """Bonus modules optionels (sport, culture), règle IUT Ville d'Avray. From 8570096eff4d0940b8f98c5dbdb37c5cf0c643d7 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 14:21:22 +0100 Subject: [PATCH 63/70] uops --- app/comp/bonus_spo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 12a382af..8d415dc1 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -579,7 +579,7 @@ class BonusTours(BonusDirect): else: self.proportion_point = 1.0 return super().compute_bonus( - self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan ) From c8459901b03680a2cf0cd44b8a1c73fc75bf6e48 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 21:43:20 +0100 Subject: [PATCH 64/70] Form. classiques: calcul de la moyenne gen. avec coefs d'UE --- app/comp/moy_ue.py | 48 +++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 289822ff..89d47175 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -36,6 +36,7 @@ from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef from app.comp import moy_mod from app.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours +from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_utils import ModuleType @@ -314,7 +315,7 @@ def compute_ue_moys_classic( nb_etuds, nb_modules = sem_matrix.shape assert len(modimpl_coefs) == nb_modules - nb_ues = len(ues) + nb_ues = len(ues) # en comptant bonus # Enlève les NaN du numérateur: sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0) @@ -329,13 +330,8 @@ def compute_ue_moys_classic( modimpl_coefs_etuds_no_nan = np.where( np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds ) - # Calcul des moyennes générales: - with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) - etud_moy_gen = np.sum( - modimpl_coefs_etuds_no_nan * sem_matrix_inscrits, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) - etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index) - # Calcul des moyennes d'UE + + # --------------------- Calcul des moyennes d'UE ue_modules = np.array( [[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues] )[..., np.newaxis][:, modimpl_mask, :] @@ -351,9 +347,35 @@ def compute_ue_moys_classic( etud_moy_ue_df = pd.DataFrame( etud_moy_ue, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues] ) - etud_coef_ue_df = pd.DataFrame( - coefs.sum(axis=2).T, - index=modimpl_inscr_df.index, # etudids - columns=[ue.id for ue in ues], - ) + + # --------------------- Calcul des moyennes générales + if sco_preferences.get_preference("use_ue_coefs", formsemestre.id): + # Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus) + etud_coef_ue_df = pd.DataFrame( + {ue.id: ue.coefficient if ue.type != UE_SPORT else 0.0 for ue in ues}, + index=modimpl_inscr_df.index, + columns=[ue.id for ue in ues], + ) + # remplace NaN par zéros dans les moyennes d'UE + etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False) + # annule les coef d'UE si la moyenne d'UE est NaN + etud_coef_ue_df_no_nan = etud_coef_ue_df.where(etud_moy_ue_df.notna(), 0.0) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_gen_s = (etud_coef_ue_df_no_nan * etud_moy_ue_df_no_nan).sum( + axis=1 + ) / etud_coef_ue_df_no_nan.sum(axis=1) + else: + # Cas normal: pondère directement les modules + etud_coef_ue_df = pd.DataFrame( + coefs.sum(axis=2).T, + index=modimpl_inscr_df.index, # etudids + columns=[ue.id for ue in ues], + ) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_gen = np.sum( + modimpl_coefs_etuds_no_nan * sem_matrix_inscrits, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + + etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index) + return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df From 2db0eb662906c407e64f7ec1918fecd47ae6fa6d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 21:53:25 +0100 Subject: [PATCH 65/70] Form. classiques: sans annuler les coefs des UE sans notes --- app/comp/moy_ue.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 89d47175..d7ed4766 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -358,12 +358,12 @@ def compute_ue_moys_classic( ) # remplace NaN par zéros dans les moyennes d'UE etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False) - # annule les coef d'UE si la moyenne d'UE est NaN - etud_coef_ue_df_no_nan = etud_coef_ue_df.where(etud_moy_ue_df.notna(), 0.0) + # Si on voulait annuler les coef d'UE dont la moyenne d'UE est NaN + # etud_coef_ue_df_no_nan = etud_coef_ue_df.where(etud_moy_ue_df.notna(), 0.0) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) - etud_moy_gen_s = (etud_coef_ue_df_no_nan * etud_moy_ue_df_no_nan).sum( + etud_moy_gen_s = (etud_coef_ue_df * etud_moy_ue_df_no_nan).sum( axis=1 - ) / etud_coef_ue_df_no_nan.sum(axis=1) + ) / etud_coef_ue_df.sum(axis=1) else: # Cas normal: pondère directement les modules etud_coef_ue_df = pd.DataFrame( From 12fcab5b76001d22dda94e5605341b1fbbab39d3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 22:03:14 +0100 Subject: [PATCH 66/70] Anciennes formules de calcul de moyenne de module: affichage + warning --- app/scodoc/sco_moduleimpl_status.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index b664c0e6..3cf49e3b 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -288,21 +288,16 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): 'Règle de calcul: moyenne=%s' % M["computation_expr"] ) - if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False): - H.append( - 'modifier' - % moduleimpl_id - ) + H.append('inutilisée dans cette version de ScoDoc') H.append("") else: H.append( - 'règle de calcul standard' + '' # règle de calcul standard' ) - if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False): - H.append( - ' (changer)' - % moduleimpl_id - ) + # if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False): + # H.append( + # f' (changer)' + # ) H.append("") H.append( 'Absences dans ce module' From c8d693ba03f926d8df15d4a21f06da66d49dc175 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 23:22:21 +0100 Subject: [PATCH 67/70] =?UTF-8?q?BUT:=20force=20le=20coef=20des=20modules?= =?UTF-8?q?=20sport=20=C3=A0=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/res_but.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 266cda97..4423f3fa 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -46,9 +46,15 @@ class ResultatsSemestreBUT(NotesTableCompat): # modimpl_coefs_df.columns.get_loc(modimpl.id) # idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id) - # Elimine les coefs des UE bonus sports XXX inutile car df_load_modimpl_coefs sans bonus - # no_bonus = [ue.type != UE_SPORT for ue in self.ues] - # modimpl_coefs_no_bonus_df = self.modimpl_coefs_df[no_bonus] + # Elimine les coefs des modimpl bonus sports: + modimpls_sport = [ + modimpl + for modimpl in self.formsemestre.modimpls_sorted + if modimpl.module.ue.type == UE_SPORT + ] + for modimpl in modimpls_sport: + self.modimpl_coefs_df[modimpl.id] = 0 + self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.sem_cube, self.etuds, @@ -63,12 +69,7 @@ class ResultatsSemestreBUT(NotesTableCompat): ) # --- Bonus Sport & Culture - modimpl_sport = [ - modimpl - for modimpl in self.formsemestre.modimpls_sorted - if modimpl.module.ue.type == UE_SPORT - ] - if len(modimpl_sport) > 0: + if len(modimpls_sport) > 0: bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: bonus: BonusSport = bonus_class( From 8b3178cd2386bed3c7c91b1cab21bc783048bc66 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 30 Jan 2022 23:52:52 +0100 Subject: [PATCH 68/70] Ordre des modules sur page modification de semestre --- app/comp/bonus_spo.py | 2 +- app/scodoc/sco_formsemestre_edit.py | 42 +++++++++-------------------- 2 files changed, 14 insertions(+), 30 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 8d415dc1..48d3d816 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -447,7 +447,7 @@ class BonusLille(BonusSportAdditif): """Calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq Les étudiants de l'IUT peuvent suivre des enseignements optionnels - de l'Université Lille 1 (sports, etc) non rattachés à une unité d'enseignement. + de l'Université Lille (sports, etc) non rattachés à une unité d'enseignement. Les points au-dessus de 10 sur 20 obtenus dans chacune des matières optionnelles sont cumulés et 4% (2% avant août 2010) de ces points cumulés diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index b1f6faa6..f1cf2264 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -35,7 +35,7 @@ from flask_login import current_user from app import db from app.auth.models import User from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN -from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids +from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns from app.models.formations import Formation from app.models.formsemestre import FormSemestre import app.scodoc.notesdb as ndb @@ -215,34 +215,18 @@ def do_formsemestre_createwithmodules(edit=False): semestre_id_labels.append("pas de semestres") else: semestre_id_labels.append(f"S{sid}") - # Liste des modules dans ce semestre de cette formation - # on pourrait faire un simple module_list( ) - # mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut: - mods = [] # liste de dicts - uelist = sco_edit_ue.ue_list({"formation_id": formation_id}) - for ue in uelist: - matlist = sco_edit_matiere.matiere_list({"ue_id": ue["ue_id"]}) - for mat in matlist: - modsmat = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]}) - # XXX debug checks - for m in modsmat: - if m["ue_id"] != ue["ue_id"]: - log( - "XXX createwithmodules: m.ue_id=%s != u.ue_id=%s !" - % (m["ue_id"], ue["ue_id"]) - ) - if m["formation_id"] != formation_id: - log( - "XXX createwithmodules: formation_id=%s\n\tm=%s" - % (formation_id, str(m)) - ) - if m["formation_id"] != ue["formation_id"]: - log( - "XXX createwithmodules: formation_id=%s\n\tue=%s\tm=%s" - % (formation_id, str(ue), str(m)) - ) - # /debug - mods = mods + modsmat + # Liste des modules dans cette formation + if formation.is_apc(): + modules = formation.modules.order_by(Module.module_type, Module.numero) + else: + modules = ( + Module.query.filter( + Module.formation_id == formation_id, UniteEns.id == Module.ue_id + ) + .order_by(Module.module_type, UniteEns.numero, Module.numero) + .all() + ) + mods = [mod.to_dict() for mod in modules] # Pour regroupement des modules par semestres: semestre_ids = {} for mod in mods: From 411533ae79353821e95202d6e1da38bdfff4eec0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 31 Jan 2022 11:56:19 +0100 Subject: [PATCH 69/70] =?UTF-8?q?Bonux=20Le=20mans:=20d=C3=A9clinaisons=20?= =?UTF-8?q?DUT/BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comp/bonus_spo.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index 48d3d816..e7f97c13 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -428,19 +428,36 @@ class BonusLeHavre(BonusSportMultiplicatif): class BonusLeMans(BonusSportAdditif): - """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans + """Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans. - La moyenne de chacune des UE du semestre sera majorée à hauteur de - 2% du cumul des points supérieurs à 10 obtenus en matières optionnelles, - dans la limite de 0,5 point. + Les points au-dessus de 10 sur 20 obtenus dans chacune des matières + optionnelles sont cumulés. + + + En BUT: la moyenne de chacune des UE du semestre est augmentée de + 2% du cumul des points de bonus, + + En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus. + + Dans tous les cas, le bonus est dans la limite de 0,5 point. """ name = "bonus_iutlemans" displayed_name = "IUT du Mans" seuil_moy_gen = 10.0 # points comptés au dessus de 10. - proportion_point = 0.02 bonus_max = 0.5 # + def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): + """calcul du bonus""" + # La date du semestre ? + if self.formsemestre.formation.is_apc(): + self.proportion_point = 0.02 + else: + self.proportion_point = 0.05 + return super().compute_bonus( + sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan + ) + # Bonus simple, mais avec changement de paramètres en 2010 ! class BonusLille(BonusSportAdditif): From 0e02baccb01d2e580c2d719c23ec59281affd2f3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 31 Jan 2022 14:17:09 +0100 Subject: [PATCH 70/70] =?UTF-8?q?Ajout=20r=C3=B4les=20et=20permissions=20p?= =?UTF-8?q?our=20Entreprises?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_permissions.py | 10 ++++++++++ app/scodoc/sco_roles_default.py | 23 +++++++++++++++++++++++ app/views/users.py | 12 +++++++++--- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index a1a06aba..c72b3ff5 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -37,6 +37,16 @@ _SCO_PERMISSIONS = ( (1 << 21, "ScoEditPVJury", "Éditer les PV de jury"), # ajouter maquettes Apogee (=> chef dept et secr): (1 << 22, "ScoEditApo", "Ajouter des maquettes Apogées"), + # Application relations entreprises + (1 << 23, "RelationsEntreprisesView", "Voir l'application relations entreprises"), + (1 << 24, "RelationsEntreprisesChange", "Modifier les entreprises"), + ( + 1 << 25, + "RelationsEntreprisesExport", + "Exporter les données de l'application relations entreprises", + ), + (1 << 25, "RelationsEntreprisesSend", "Envoyer des offres"), + (1 << 26, "RelationsEntreprisesValidate", "Valide les entreprises"), ) diff --git a/app/scodoc/sco_roles_default.py b/app/scodoc/sco_roles_default.py index 6f8d79ee..9529b031 100644 --- a/app/scodoc/sco_roles_default.py +++ b/app/scodoc/sco_roles_default.py @@ -58,8 +58,31 @@ SCO_ROLES_DEFAULTS = { # il peut ajouter des tags sur les formations: # (doit avoir un rôle Ens en plus !) "RespPe": (p.ScoEditFormationTags,), + # Rôles pour l'application relations entreprises + # ObservateurEntreprise est un observateur de l'application entreprise + "ObservateurEntreprise": (p.RelationsEntreprisesView,), + # UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification) + "UtilisateurEntreprise": (p.RelationsEntreprisesView, p.RelationsEntreprisesChange), + # AdminEntreprise est un admin de l'application entreprise (toutes les actions possibles de l'application) + "AdminEntreprise": ( + p.RelationsEntreprisesView, + p.RelationsEntreprisesChange, + p.RelationsEntreprisesExport, + p.RelationsEntreprisesSend, + p.RelationsEntreprisesValidate, + ), # Super Admin est un root: création/suppression de départements # _tous_ les droits # Afin d'avoir tous les droits, il ne doit pas être asscoié à un département "SuperAdmin": p.ALL_PERMISSIONS, } + +# Les rôles accessibles via la page d'admin utilisateurs +# - associés à un département: +ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe") +# - globaux: (ne peuvent être attribués que par un SuperAdmin) +ROLES_ATTRIBUABLES_SCODOC = ( + "ObservateurEntreprise", + "UtilisateurEntreprise", + "AdminEntreprise", +) diff --git a/app/views/users.py b/app/views/users.py index 59175344..0655da50 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -62,7 +62,7 @@ from app.decorators import ( permission_required, ) -from app.scodoc import html_sco_header, sco_import_users, sco_excel +from app.scodoc import html_sco_header, sco_import_users, sco_excel, sco_roles_default from app.scodoc import sco_users from app.scodoc import sco_utils as scu from app.scodoc import sco_xml @@ -150,9 +150,10 @@ def user_info(user_name, format="json"): @permission_required(Permission.ScoUsersAdmin) @scodoc7func def create_user_form(user_name=None, edit=0, all_roles=1): - "form. création ou edition utilisateur" + "form. création ou édition utilisateur" if user_name is not None: # scodoc7func converti en int ! user_name = str(user_name) + Role.insert_roles() # assure la mise à jour des rôles en base auth_dept = current_user.dept from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email initvalues = {} @@ -191,7 +192,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1): else: # Les rôles standards créés à l'initialisation de ScoDoc: standard_roles = [ - Role.get_named_role(r) for r in ("Ens", "Secr", "Admin", "RespPe") + Role.get_named_role(r) for r in sco_roles_default.ROLES_ATTRIBUABLES_DEPT ] # Départements auxquels ont peut associer des rôles via ce dialogue: # si SuperAdmin, tous les rôles standards dans tous les départements @@ -215,6 +216,11 @@ def create_user_form(user_name=None, edit=0, all_roles=1): editable_roles_set = { (r, dept) for r in standard_roles for dept in administrable_dept_acronyms } + if current_user.is_administrator(): + editable_roles_set |= { + (Role.get_named_role(r), "") + for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC + } # if not edit: submitlabel = "Créer utilisateur"