diff --git a/app/__init__.py b/app/__init__.py
index a976da7f7..a1862aaa7 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -10,10 +10,11 @@ import traceback
import logging
from logging.handlers import SMTPHandler, WatchedFileHandler
+from threading import Thread
from flask import current_app, g, request
from flask import Flask
-from flask import abort, has_request_context, jsonify
+from flask import abort, flash, has_request_context, jsonify
from flask import render_template
from flask.logging import default_handler
from flask_sqlalchemy import SQLAlchemy
@@ -27,6 +28,7 @@ import sqlalchemy
from app.scodoc.sco_exceptions import (
AccessDenied,
+ ScoBugCatcher,
ScoGenError,
ScoValueError,
APIInvalidParams,
@@ -43,11 +45,13 @@ mail = Mail()
bootstrap = Bootstrap()
moment = Moment()
-cache = Cache( # XXX TODO: configuration file
+CACHE_TYPE = os.environ.get("CACHE_TYPE")
+cache = Cache(
config={
# see https://flask-caching.readthedocs.io/en/latest/index.html#configuring-flask-caching
- "CACHE_TYPE": "RedisCache",
- "CACHE_DEFAULT_TIMEOUT": 0, # by default, never expire
+ "CACHE_TYPE": CACHE_TYPE or "RedisCache",
+ # by default, never expire:
+ "CACHE_DEFAULT_TIMEOUT": os.environ.get("CACHE_DEFAULT_TIMEOUT") or 0,
}
)
@@ -60,7 +64,7 @@ def handle_access_denied(exc):
return render_template("error_access_denied.html", exc=exc), 403
-def internal_server_error(e):
+def internal_server_error(exc):
"""Bugs scodoc, erreurs 500"""
# note that we set the 500 status explicitly
return (
@@ -68,11 +72,35 @@ def internal_server_error(e):
"error_500.html",
SCOVERSION=sco_version.SCOVERSION,
date=datetime.datetime.now().isoformat(),
+ exc=exc,
+ request_url=request.url,
),
500,
)
+def handle_sco_bug(exc):
+ """Un bug, en général rare, sur lequel les dev cherchent des
+ informations pour le corriger.
+ """
+ Thread(
+ target=_async_dump, args=(current_app._get_current_object(), request.url)
+ ).start()
+
+ return internal_server_error(exc)
+
+
+def _async_dump(app, request_url: str):
+ from app.scodoc.sco_dump_db import sco_dump_and_send_db
+
+ with app.app_context():
+ ndb.open_db_connection()
+ try:
+ sco_dump_and_send_db("ScoBugCatcher", request_url=request_url)
+ except ScoValueError:
+ pass
+
+
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
@@ -187,11 +215,12 @@ def create_app(config_class=DevConfig):
moment.init_app(app)
cache.init_app(app)
sco_cache.CACHE = cache
+ if CACHE_TYPE: # non default
+ app.logger.info(f"CACHE_TYPE={CACHE_TYPE}")
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error)
- app.register_error_handler(404, handle_sco_value_error)
-
+ app.register_error_handler(ScoBugCatcher, handle_sco_bug)
app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error)
@@ -337,7 +366,7 @@ def user_db_init():
current_app.logger.info("Init User's db")
# Create roles:
- Role.insert_roles()
+ Role.reset_standard_roles_permissions()
current_app.logger.info("created initial roles")
# Ensure that admin exists
admin_mail = current_app.config.get("SCODOC_ADMIN_MAIL")
@@ -460,15 +489,12 @@ from app.models import Departement
from app.scodoc import notesdb as ndb, sco_preferences
from app.scodoc import sco_cache
-# admin_role = Role.query.filter_by(name="SuperAdmin").first()
-# if admin_role:
-# admin = (
-# User.query.join(UserRole)
-# .filter((UserRole.user_id == User.id) & (UserRole.role_id == admin_role.id))
-# .first()
-# )
-# else:
-# click.echo(
-# "Warning: user database not initialized !\n (use: flask user-db-init)"
-# )
-# admin = None
+
+def scodoc_flash_status_messages():
+ """Should be called on each page: flash messages indicating specific ScoDoc status"""
+ email_test_mode_address = sco_preferences.get_preference("email_test_mode_address")
+ if email_test_mode_address:
+ flash(
+ f"Mode test: mails redirigés vers {email_test_mode_address}",
+ category="warning",
+ )
diff --git a/app/auth/models.py b/app/auth/models.py
index 329bc3868..cfab21a9c 100644
--- a/app/auth/models.py
+++ b/app/auth/models.py
@@ -173,7 +173,7 @@ class User(UserMixin, db.Model):
"id": self.id,
"active": self.active,
"status_txt": "actif" if self.active else "fermé",
- "last_seen": self.last_seen.isoformat() + "Z",
+ "last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else "",
"nom": (self.nom or ""), # sco8
"prenom": (self.prenom or ""), # sco8
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
@@ -270,6 +270,8 @@ class User(UserMixin, db.Model):
"""Add a role to this user.
:param role: Role to add.
"""
+ if not isinstance(role, Role):
+ raise ScoValueError("add_role: rôle invalide")
self.user_roles.append(UserRole(user=self, role=role, dept=dept))
def add_roles(self, roles, dept):
@@ -281,7 +283,9 @@ class User(UserMixin, db.Model):
def set_roles(self, roles, dept):
"set roles in the given dept"
- self.user_roles = [UserRole(user=self, role=r, dept=dept) for r in roles]
+ self.user_roles = [
+ UserRole(user=self, role=r, dept=dept) for r in roles if isinstance(r, Role)
+ ]
def get_roles(self):
"iterator on my roles"
@@ -292,7 +296,11 @@ class User(UserMixin, db.Model):
"""string repr. of user's roles (with depts)
e.g. "Ens_RT, Ens_Info, Secr_CJ"
"""
- return ",".join(f"{r.role.name or ''}_{r.dept or ''}" for r in self.user_roles)
+ return ",".join(
+ f"{r.role.name or ''}_{r.dept or ''}"
+ for r in self.user_roles
+ if r is not None
+ )
def is_administrator(self):
"True if i'm an active SuperAdmin"
@@ -402,20 +410,30 @@ class Role(db.Model):
return self.permissions & perm == perm
@staticmethod
- def insert_roles():
- """Create default roles"""
+ def reset_standard_roles_permissions(reset_permissions=True):
+ """Create default roles if missing, then, if reset_permissions,
+ reset their permissions to default values.
+ """
default_role = "Observateur"
for role_name, permissions in SCO_ROLES_DEFAULTS.items():
role = Role.query.filter_by(name=role_name).first()
if role is None:
role = Role(name=role_name)
- role.reset_permissions()
- for perm in permissions:
- role.add_permission(perm)
- role.default = role.name == default_role
- db.session.add(role)
+ role.default = role.name == default_role
+ db.session.add(role)
+ if reset_permissions:
+ role.reset_permissions()
+ for perm in permissions:
+ role.add_permission(perm)
+ db.session.add(role)
+
db.session.commit()
+ @staticmethod
+ def ensure_standard_roles():
+ """Create default roles if missing"""
+ Role.reset_standard_roles_permissions(reset_permissions=False)
+
@staticmethod
def get_named_role(name):
"""Returns existing role with given name, or None."""
diff --git a/app/auth/routes.py b/app/auth/routes.py
index df3401515..24daa8ca0 100644
--- a/app/auth/routes.py
+++ b/app/auth/routes.py
@@ -19,7 +19,7 @@ from app.auth.forms import (
ResetPasswordForm,
DeactivateUserForm,
)
-from app.auth.models import Permission
+from app.auth.models import Role
from app.auth.models import User
from app.auth.email import send_password_reset_email
from app.decorators import admin_required
@@ -121,3 +121,11 @@ def reset_password(token):
flash(_("Votre mot de passe a été changé."))
return redirect(url_for("auth.login"))
return render_template("auth/reset_password.html", form=form, user=user)
+
+
+@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
+@admin_required
+def reset_standard_roles_permissions():
+ Role.reset_standard_roles_permissions()
+ flash("rôles standard réinitialisés !")
+ return redirect(url_for("scodoc.configuration"))
diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index 87ec62a28..23fd8c587 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -7,16 +7,19 @@
"""Génération bulletin BUT
"""
+import collections
import datetime
+import numpy as np
from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT
-from app.models import FormSemestre, Identite, formsemestre
+from app.models import FormSemestre, Identite
+from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_preferences
-from app.scodoc.sco_codes_parcours import UE_SPORT
+from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_utils import fmt_note
@@ -61,18 +64,15 @@ class BulletinBUT:
# }
return d
- def etud_ue_results(self, etud, ue):
+ def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict:
"dict synthèse résultats UE"
res = self.res
+
d = {
"id": ue.id,
"titre": ue.titre,
"numero": ue.numero,
"type": ue.type,
- "ECTS": {
- "acquis": 0, # XXX TODO voir jury #sco92
- "total": ue.ects,
- },
"color": ue.color,
"competence": None, # XXX TODO lien avec référentiel
"moyenne": None,
@@ -80,11 +80,16 @@ class BulletinBUT:
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
if res.bonus_ues is not None and ue.id in res.bonus_ues
else fmt_note(0.0),
- "malus": res.malus[ue.id][etud.id],
+ "malus": fmt_note(res.malus[ue.id][etud.id]),
"capitalise": None, # "AAAA-MM-JJ" TODO #sco92
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
"saes": self.etud_ue_mod_results(etud, ue, res.saes),
}
+ if self.prefs["bul_show_ects"]:
+ d["ECTS"] = {
+ "acquis": decision_ue.get("ects", 0.0),
+ "total": ue.ects or 0.0, # float même si non renseigné
+ }
if ue.type != UE_SPORT:
if self.prefs["bul_show_ue_rangs"]:
rangs, effectif = res.ue_rangs[ue.id]
@@ -111,9 +116,10 @@ class BulletinBUT:
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
return d
- def etud_mods_results(self, etud, modimpls) -> dict:
+ def etud_mods_results(self, etud, modimpls, version="long") -> dict:
"""dict synthèse résultats des modules indiqués,
- avec évaluations de chacun."""
+ avec évaluations de chacun (sauf si version == "short")
+ """
res = self.res
d = {}
# etud_idx = self.etud_index[etud.id]
@@ -154,12 +160,14 @@ class BulletinBUT:
"evaluations": [
self.etud_eval_results(etud, e)
for e in modimpl.evaluations
- if e.visibulletin
+ if (e.visibulletin or version == "long")
and (
modimpl_results.evaluations_etat[e.id].is_complete
or self.prefs["bul_show_all_evals"]
)
- ],
+ ]
+ if version != "short"
+ else [],
}
return d
@@ -168,14 +176,23 @@ class BulletinBUT:
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
+ modimpls_evals_poids = self.res.modimpls_evals_poids[e.moduleimpl_id]
+ try:
+ poids = {
+ ue.acronyme: modimpls_evals_poids[ue.id][e.id]
+ for ue in self.res.ues
+ if ue.type != UE_SPORT
+ }
+ except KeyError:
+ poids = collections.defaultdict(lambda: 0.0)
d = {
"id": e.id,
"description": e.description,
"date": e.jour.isoformat() if e.jour else None,
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
- "coef": e.coefficient,
- "poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
+ "coef": fmt_note(e.coefficient),
+ "poids": poids,
"note": {
"value": fmt_note(
eval_notes[etud.id],
@@ -205,7 +222,8 @@ class BulletinBUT:
details = [
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
for ue in res.ues
- if res.modimpls_in_ue(ue.id, etudid)
+ if ue.type != UE_SPORT
+ and res.modimpls_in_ue(ue.id, etudid)
and ue.id in res.bonus_ues
and bonus_vect[ue.id] > 0.0
]
@@ -217,14 +235,22 @@ class BulletinBUT:
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
def bulletin_etud(
- self, etud: Identite, formsemestre: FormSemestre, force_publishing=False
+ self,
+ etud: Identite,
+ formsemestre: FormSemestre,
+ force_publishing=False,
+ version="long",
) -> dict:
"""Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
+ - version:
+ "long", "selectedevals": toutes les infos (notes des évaluations)
+ "short" : ne descend pas plus bas que les modules.
+
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
(bulletins non publiés).
"""
res = self.res
- etat_inscription = etud.etat_inscription(formsemestre.id)
+ etat_inscription = etud.inscription_etat(formsemestre.id)
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
published = (not formsemestre.bul_hide_xml) or force_publishing
d = {
@@ -257,39 +283,55 @@ class BulletinBUT:
"numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [], # XXX TODO
- "absences": {
+ }
+ if self.prefs["bul_show_abs"]:
+ semestre_infos["absences"] = {
"injustifie": nbabs - nbabsjust,
"total": nbabs,
- },
- }
+ }
+ decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
+ if self.prefs["bul_show_ects"]:
+ ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0
+ ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
+ semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
semestre_infos.update(
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
)
if etat_inscription == scu.INSCRIT:
- semestre_infos.update(
- {
- "notes": { # moyenne des moyennes générales du semestre
- "value": fmt_note(res.etud_moy_gen[etud.id]),
- "min": fmt_note(res.etud_moy_gen.min()),
- "moy": fmt_note(res.etud_moy_gen.mean()),
- "max": fmt_note(res.etud_moy_gen.max()),
- },
- "rang": { # classement wrt moyenne général, indicatif
- "value": res.etud_moy_gen_ranks[etud.id],
- "total": nb_inscrits,
- },
- },
- )
+ # moyenne des moyennes générales du semestre
+ semestre_infos["notes"] = {
+ "value": fmt_note(res.etud_moy_gen[etud.id]),
+ "min": fmt_note(res.etud_moy_gen.min()),
+ "moy": fmt_note(res.etud_moy_gen.mean()),
+ "max": fmt_note(res.etud_moy_gen.max()),
+ }
+ if self.prefs["bul_show_rangs"] and not np.isnan(res.etud_moy_gen[etud.id]):
+ # classement wrt moyenne général, indicatif
+ semestre_infos["rang"] = {
+ "value": res.etud_moy_gen_ranks[etud.id],
+ "total": nb_inscrits,
+ }
+ else:
+ semestre_infos["rang"] = {
+ "value": "-",
+ "total": nb_inscrits,
+ }
d.update(
{
- "ressources": self.etud_mods_results(etud, res.ressources),
- "saes": self.etud_mods_results(etud, res.saes),
+ "ressources": self.etud_mods_results(
+ etud, res.ressources, version=version
+ ),
+ "saes": self.etud_mods_results(etud, res.saes, version=version),
"ues": {
- ue.acronyme: self.etud_ue_results(etud, ue)
+ ue.acronyme: self.etud_ue_results(
+ etud, ue, decision_ue=decisions_ues.get(ue.id, {})
+ )
for ue in res.ues
- if self.res.modimpls_in_ue(
- ue.id, etud.id
- ) # si l'UE comporte des modules auxquels on est inscrit
+ # si l'UE comporte des modules auxquels on est inscrit:
+ if (
+ (ue.type == UE_SPORT)
+ or self.res.modimpls_in_ue(ue.id, etud.id)
+ )
},
"semestre": semestre_infos,
},
@@ -317,20 +359,45 @@ class BulletinBUT:
return d
- def bulletin_etud_complet(self, etud: Identite) -> dict:
- """Bulletin dict complet avec toutes les infos pour les bulletins pdf"""
- d = self.bulletin_etud(etud, self.res.formsemestre, force_publishing=True)
+ def bulletin_etud_complet(self, etud: Identite, version="long") -> dict:
+ """Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
+ Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
+ """
+ d = self.bulletin_etud(
+ etud, self.res.formsemestre, version=version, force_publishing=True
+ )
d["etudid"] = etud.id
d["etud"] = d["etudiant"]
d["etud"]["nomprenom"] = etud.nomprenom
d.update(self.res.sem)
+ etud_etat = self.res.get_etud_etat(etud.id)
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
- self.res.get_etud_etat(etud.id),
+ etud_etat,
self.prefs,
decision_sem=d["semestre"].get("decision_sem"),
)
+ if etud_etat == scu.DEMISSION:
+ d["demission"] = "(Démission)"
+ elif etud_etat == DEF:
+ d["demission"] = "(Défaillant)"
+ else:
+ d["demission"] = ""
+
# --- Absences
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
+
+ # --- Decision Jury
+ infos, dpv = sco_bulletins.etud_descr_situation_semestre(
+ etud.id,
+ self.res.formsemestre.id,
+ format="html",
+ show_date_inscr=self.prefs["bul_show_date_inscr"],
+ show_decisions=self.prefs["bul_show_decision"],
+ show_uevalid=self.prefs["bul_show_uevalid"],
+ show_mention=self.prefs["bul_show_mention"],
+ )
+
+ d.update(infos)
# --- Rangs
d[
"rang_nt"
@@ -341,5 +408,6 @@ class BulletinBUT:
d.update(
sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
)
- # XXX TODO A COMPLETER ?
+ d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
+
return d
diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py
index 5003e216e..29b9d6749 100644
--- a/app/but/bulletin_but_pdf.py
+++ b/app/but/bulletin_but_pdf.py
@@ -6,22 +6,13 @@
"""Génération bulletin BUT au format PDF standard
"""
-
-import datetime
-from app.scodoc.sco_pdf import blue, cm, mm
-
-from flask import url_for, g
-from app.models.formsemestre import FormSemestre
-
-from app.scodoc import gen_tables
-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
+from reportlab.lib.colors import blue
+from reportlab.lib.units import cm, mm
+from reportlab.platypus import Paragraph, Spacer
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
+from app.scodoc import gen_tables
+from app.scodoc.sco_codes_parcours import UE_SPORT
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
@@ -31,6 +22,9 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"""
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
+ scale_table_in_page = False # pas de mise à l'échelle pleine page auto
+ multi_pages = True # plusieurs pages par bulletins
+ small_fontsize = "8"
def bul_table(self, format="html"):
"""Génère la table centrale du bulletin de notes
@@ -38,31 +32,38 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
- en HTML: une chaine
- en PDF: une liste d'objets PLATYPUS (eg instance de Table).
"""
- formsemestre_id = self.infos["formsemestre_id"]
- (
- synth_col_keys,
- synth_P,
- synth_pdf_style,
- synth_col_widths,
- ) = self.but_table_synthese()
- #
- table_synthese = gen_tables.GenTable(
- rows=synth_P,
- columns_ids=synth_col_keys,
- pdf_table_style=synth_pdf_style,
- pdf_col_widths=[synth_col_widths[k] for k in synth_col_keys],
- preferences=self.preferences,
- html_class="notes_bulletin",
- html_class_ignore_default=True,
- html_with_td_classes=True,
- )
- # Ici on ajoutera table des ressources, tables des UE
- # TODO
+ tables_infos = [
+ # ---- TABLE SYNTHESE UES
+ self.but_table_synthese_ues(),
+ ]
+ if self.version != "short":
+ tables_infos += [
+ # ---- TABLE RESSOURCES
+ self.but_table_ressources(),
+ # ---- TABLE SAE
+ self.but_table_saes(),
+ ]
+ objects = []
+ for i, (col_keys, rows, pdf_style, col_widths) in enumerate(tables_infos):
+ table = gen_tables.GenTable(
+ rows=rows,
+ columns_ids=col_keys,
+ pdf_table_style=pdf_style,
+ pdf_col_widths=[col_widths[k] for k in col_keys],
+ preferences=self.preferences,
+ html_class="notes_bulletin",
+ html_class_ignore_default=True,
+ html_with_td_classes=True,
+ )
+ table_objects = table.gen(format=format)
+ objects += table_objects
+ # objects += [KeepInFrame(0, 0, table_objects, mode="shrink")]
+ if i != 2:
+ objects.append(Spacer(1, 6 * mm))
- # XXX à modifier pour générer plusieurs tables:
- return table_synthese.gen(format=format)
+ return objects
- def but_table_synthese(self):
+ def but_table_synthese_ues(self, title_bg=(182, 235, 255)):
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
et leurs coefs.
Renvoie: colkeys, P, pdf_style, colWidths
@@ -76,41 +77,270 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"moyenne": 2 * cm,
"coef": 2 * cm,
}
- P = [] # elems pour générer table avec gen_table (liste de dicts)
- col_keys = ["titre", "moyenne"] # noms des colonnes à afficher
- for ue_acronym, ue in self.infos["ues"].items():
- # 1er ligne titre UE
- moy_ue = ue.get("moyenne")
- t = {
- "titre": f"{ue_acronym} - {ue['titre']}",
- "moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-",
- "_css_row_class": "note_bold",
- "_pdf_row_markup": ["b"],
- "_pdf_style": [],
- }
- P.append(t)
- # 2eme ligne titre UE (bonus/malus/ects)
- t = {
- "titre": "",
- "moyenne": f"""Bonus: {ue['bonus']} - Malus: {
- ue["malus"]} - ECTS: {ue["ECTS"]["acquis"]} / {ue["ECTS"]["total"]}""",
+ title_bg = tuple(x / 255.0 for x in title_bg)
+ nota_bene = "La moyenne des ressources et SAÉs dans une UE dépend des poids donnés aux évaluations."
+ # elems pour générer table avec gen_table (liste de dicts)
+ rows = [
+ # Ligne de titres
+ {
+ "titre": "Unités d'enseignement",
+ "moyenne": Paragraph("
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. @@ -476,6 +474,29 @@ class BonusBordeaux1(BonusSportMultiplicatif): amplitude = 0.005 +# Exactement le même que Bordeaux: +class BonusBrest(BonusSportMultiplicatif): + """Calcul bonus modules optionnels (sport, culture), règle IUT de Brest, + sur moyenne générale et UEs. +
+ Les étudiants de l'IUT peuvent suivre des enseignements optionnels + de l'Université (sport, théâtre) non rattachés à une unité d'enseignement. +
+ 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 : pourcentage = (points au dessus de 10) / 2
+
+ Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale. +
+ """ + + name = "bonus_iut_brest" + displayed_name = "IUT de Brest" + classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP + seuil_moy_gen = 10.0 + amplitude = 0.005 + + class BonusCachan1(BonusSportAdditif): """Calcul bonus optionnels (sport, culture), règle IUT de Cachan 1. @@ -486,14 +507,14 @@ class BonusCachan1(BonusSportAdditif):+ Soit N la note attribuée, le bonus (ou malus) correspond à : + (N-10) x 0,05 + appliqué sur chaque UE du semestre sélectionné pour le BUT + ou appliqué sur la moyenne générale du semestre sélectionné pour le DUT. +
+Exemples :
+