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 d975766ef..dd872a7aa 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 0fa105db8..d0a5407dd 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 cab845d19..23676c26f 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 dd872a7aa..5f64f57b2 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 03f656a7c..976443a3e 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 597b9baf6..75f5e9a3b 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 ab27ecdae..7a60b67ae 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
),
- """
{%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.
+
{% 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 a7aedaa88..02bc1fb11 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 cc9b9db01..fe65348cf 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 976443a3e..f18f0892b 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 000000000..43ed62824
--- /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 000000000..af04ee51e
--- /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 000000000..0c2f32b24
--- /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 36fffe4e4..39b1b3401 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 87d50e3f7..b06a8e469 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 39b1b3401..3c171d1f5 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 b06a8e469..e14ae5266 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 d0a5407dd..9ccbd9c8d 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 02bc1fb11..394dfe833 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 35c9ca837..8b0509666 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 e2273c3b6..b69d566a6 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 3c171d1f5..55a2c7850 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 43ed62824..a655f450f 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 38d3e4fe8..6bcb8cc32 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 86525c5c8..3f4eb79f8 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 d193b3733..e2f28c695 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 8b0509666..4be64afbb 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 8f187b7e9..32768df03 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 fe65348cf..591753448 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 2c6288bb4..da7677d45 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 f318f236e..69b8f5686 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 6a758a64b..07517f362 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 000000000..bba0cd47a
--- /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 8a5f4bc8f..b9be1e9f0 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 2fee521d3..c3596f92a 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 3c658988b..ae167d4e7 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 ae8b98cca..cee1b8881 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 669380a31..2ae263d5b 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 68972ced6..3c7428673 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 1ff654686..9356bf89e 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 16be84518..26548f08d 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 a74f1671d..e1febc321 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 2aa36da9c..d51a620b0 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 24e552461..ac82b127e 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 59c82ec80..f220ee177 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 26223eefc..3497414c0 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 722b7ec9a..23b500393 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 75b08b501..5351ecb33 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 3306b80d1..65101f1f4 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 eec178f14..090f18e01 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 1941d051f..4dcf4d819 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 5a6d542d0..89f655049 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 aa2b95775..5d6ba7def 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 17ddd09b6..59ebab2d5 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 c3ccc9cb6..949567eca 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 6b7884591..5282c5ce1 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 d0ee4b1a7..2c5a8af43 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -1107,6 +1107,7 @@ _TABLEAU_MODULES_HEAD = """
- {% 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 %}
-
-
-
-{% 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 ea2f2a419..234d688c5 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 451ebee2d..a18251fdc 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 1c45d6e0e..79d3b776f 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 9356bf89e..e2e5ab16a 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 95e18156c..c81f36782 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 c81f36782..5df158068 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 a495f9650..2c9dd2301 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 43a00ebe5..5a98d375f 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 bba0cd47a..ee54262d9 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 ee54262d9..15d4726b8 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 5351ecb33..1e754f82e 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 15d4726b8..15472b593 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 15472b593..7427089d0 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 2ae263d5b..e1fcdc272 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 3c7428673..65c83e641 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 5827703c7..209a2a017 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 7427089d0..0e63adaff 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 c69b5ae60..da98998c4 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 e1fcdc272..e7c6d4c45 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 65c83e641..8d52d0c32 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 0e63adaff..d442647a3 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 5c3189898..cf72ed188 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 9540dbe55..8ef59e41f 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 c9a2da171..1de9969ef 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 8ef59e41f..00415b082 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 17d47915a..6586ddd09 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 5f64f57b2..112658e65 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 00415b082..c3e725205 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 b575123ec..8359d12bc 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 a18251fdc..28dfe7cfe 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 baf5de70b..8cd7e5e99 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 2c9dd2301..5f58428bb 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 f4dc60fc9..c44dcdb8e 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 d442647a3..d6abcb785 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 d6abcb785..e822394db 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 e0b349e49..d6536a8de 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 da98998c4..9c9c56385 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 1e754f82e..5fc3b8b4d 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 788f4b931..5c4741f71 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 684df8a08..c5758cb5f 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 34e8e9c8b..8424496ca 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 888d95914..823772de5 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 234d688c5..0b37da544 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 05a6320f1..302568fd8 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 5df158068..11eb7e65e 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 e822394db..9be542c58 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 11eb7e65e..b9e334786 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 9be542c58..e9f615cf5 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 1d8ff453e..1a64809fa 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 cee1b8881..289822ff2 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 e7c6d4c45..266cda97c 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 e2e5ab16a..a90ce1c12 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 b69d566a6..edd57097d 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 4dcf4d819..23833f88c 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 3f132d6a4..a20c8bfa3 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 521e97da7..6e18ad575 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 += `
"
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 5c4741f71..5b333a9fd 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 8359d12bc..4053bde0e 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 4053bde0e..1c42d53fc 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:
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 1a64809fa..bf4afe7a0 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 a6016031e..d059701ae 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 f8e82e414..12a382afa 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 12a382afa..8d415dc10 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 289822ff2..89d47175d 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 89d47175d..d7ed47667 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 b664c0e6a..3cf49e3b5 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 266cda97c..4423f3fa1 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 8d415dc10..48d3d816a 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 b1f6faa68..f1cf22644 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 48d3d816a..e7f97c13d 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 a1a06aba8..c72b3ff50 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 6f8d79eee..9529b031c 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 591753448..0655da501 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"