diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py
index f64769fc..a533fb80 100644
--- a/app/but/bulletin_but.py
+++ b/app/but/bulletin_but.py
@@ -80,7 +80,7 @@ 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),
@@ -177,7 +177,7 @@ class BulletinBUT:
"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,
+ "coef": fmt_note(e.coefficient),
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
"note": {
"value": fmt_note(
diff --git a/app/but/bulletin_but_pdf.py b/app/but/bulletin_but_pdf.py
index 5003e216..6a317684 100644
--- a/app/but/bulletin_but_pdf.py
+++ b/app/but/bulletin_but_pdf.py
@@ -6,20 +6,13 @@
"""Génération bulletin BUT au format PDF standard
"""
+import itertools
+from reportlab.platypus import KeepInFrame, Paragraph, Spacer
-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 app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
@@ -31,6 +24,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"""
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
+ scale_table_in_page = False
def bul_table(self, format="html"):
"""Génère la table centrale du bulletin de notes
@@ -38,31 +32,35 @@ 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(),
+ # ---- 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,8 +74,30 @@ 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
+ title_bg = tuple(x / 255.0 for x in title_bg)
+ # elems pour générer table avec gen_table (liste de dicts)
+ rows = [
+ # Ligne de titres
+ {
+ "titre": "Unités d'enseignement",
+ "moyenne": "Note/20",
+ "coef": "Coef.",
+ "_css_row_class": "note_bold",
+ "_pdf_row_markup": ["b"],
+ "_pdf_style": [
+ ("BACKGROUND", (0, 0), (-1, 0), title_bg),
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
+ (
+ "LINEBELOW",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ blue,
+ ),
+ ],
+ }
+ ]
+ col_keys = ["titre", "moyenne", "coef"] # noms des colonnes à afficher
for ue_acronym, ue in self.infos["ues"].items():
# 1er ligne titre UE
moy_ue = ue.get("moyenne")
@@ -86,31 +106,164 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-",
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
- "_pdf_style": [],
+ "_pdf_style": [
+ (
+ "LINEABOVE",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ self.PDF_LINECOLOR,
+ ),
+ ("BACKGROUND", (0, 0), (-1, 0), title_bg),
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
+ ],
}
- P.append(t)
+ rows.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"]}""",
- "_css_row_class": "note_bold",
- "_pdf_row_markup": ["b"],
+ "titre": f"""Bonus: {ue['bonus']} - Malus: {
+ ue["malus"]}""",
+ "moyenne": f"""ECTS: {ue["ECTS"]["acquis"]} / {ue["ECTS"]["total"]}""",
+ "_moyenne_colspan": 2,
+ # "_css_row_class": "",
+ # "_pdf_row_markup": [""],
"_pdf_style": [
+ ("ALIGN", (0, 0), (1, 0), "RIGHT"),
+ ("TEXTCOLOR", (0, 0), (-1, 0), blue),
+ ("BACKGROUND", (0, 0), (-1, 0), title_bg),
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
self.PDF_LINECOLOR,
- )
+ ),
],
}
- P.append(t)
-
+ rows.append(t)
+ # Liste chaque ressource puis SAE
+ for mod_type in ("ressources", "saes"):
+ for mod_code, mod in ue[mod_type].items():
+ t = {
+ "titre": f"{mod_code} {self.infos[mod_type][mod_code]['titre']}",
+ "moyenne": mod["moyenne"],
+ "coef": mod["coef"],
+ "_coef_pdf": Paragraph(
+ f"{mod['coef']}"
+ ),
+ "_pdf_style": [
+ (
+ "LINEBELOW",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ (0.7, 0.7, 0.7), # gris clair
+ )
+ ],
+ }
+ rows.append(t)
# Global pdf style commands:
pdf_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
]
- return col_keys, P, pdf_style, col_widths
+ return col_keys, rows, pdf_style, col_widths
+
+ def but_table_ressources(self):
+ """La table de synthèse; pour chaque ressources, note et liste d'évaluations
+ Renvoie: colkeys, P, pdf_style, colWidths
+ """
+ return self.bul_table_modules(
+ mod_type="ressources", title="Ressources", title_bg=(248, 200, 68)
+ )
+
+ def but_table_saes(self):
+ "table des SAEs"
+ return self.bul_table_modules(
+ mod_type="saes",
+ title="Situations d'apprentissage et d'évaluation",
+ title_bg=(198, 255, 171),
+ )
+
+ def bul_table_modules(self, mod_type=None, title="", title_bg=(248, 200, 68)):
+ """Table ressources ou SAEs
+ - colkeys: nom des colonnes de la table (clés)
+ - P : table (liste de dicts de chaines de caracteres)
+ - pdf_style : commandes table Platypus
+ - largeurs de colonnes pour PDF
+ """
+ col_widths = {
+ "titre": None,
+ "moyenne": 2 * cm,
+ "coef": 2 * cm,
+ }
+ title_bg = tuple(x / 255.0 for x in title_bg)
+ # elems pour générer table avec gen_table (liste de dicts)
+ rows = [
+ # Ligne de titres
+ {
+ "titre": title,
+ "moyenne": "Note/20",
+ "coef": "Coef.",
+ "_css_row_class": "note_bold",
+ "_pdf_row_markup": ["b"],
+ "_pdf_style": [
+ ("BACKGROUND", (0, 0), (-1, 0), title_bg),
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
+ (
+ "LINEBELOW",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ blue,
+ ),
+ ],
+ }
+ ]
+ col_keys = ["titre", "moyenne", "coef"] # noms des colonnes à afficher
+ for mod_code, mod in self.infos[mod_type].items():
+ # 1er ligne titre module
+ t = {
+ "titre": f"{mod_code} - {mod['titre']}",
+ "moyenne": "", # pas de moyenne
+ "_css_row_class": "note_bold",
+ "_pdf_row_markup": ["b"],
+ "_pdf_style": [
+ (
+ "LINEABOVE",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ self.PDF_LINECOLOR,
+ ),
+ ("BACKGROUND", (0, 0), (-1, 0), title_bg),
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 7),
+ ],
+ }
+ rows.append(t)
+ # Evaluations:
+ for e in mod["evaluations"]:
+ t = {
+ "titre": f"{e['description']}",
+ "moyenne": e["note"]["value"],
+ "coef": e["coef"],
+ "_coef_pdf": Paragraph(
+ f"{e['coef']}"
+ ),
+ "_pdf_style": [
+ (
+ "LINEBELOW",
+ (0, 0),
+ (-1, 0),
+ self.PDF_LINEWIDTH,
+ (0.7, 0.7, 0.7), # gris clair
+ )
+ ],
+ }
+ rows.append(t)
+ # Global pdf style commands:
+ pdf_style = [
+ ("VALIGN", (0, 0), (-1, -1), "TOP"),
+ ("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
+ ]
+ return col_keys, rows, pdf_style, col_widths
diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index 2feb0577..b7c506a0 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -862,6 +862,51 @@ class BonusStDenis(BonusSportAdditif):
bonus_max = 0.5
+class BonusTarbes(BonusSportAdditif):
+ """Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
+
+
+ - Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
+ La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
+
+ - Le trentième des points au dessus de 10 est ajouté à la moyenne des UE.
+
+ - Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
+ sur chaque UE.
+
+
+ """
+
+ name = "bonus_tarbes"
+ displayed_name = "IUT de Tazrbes"
+ seuil_moy_gen = 10.0
+ proportion_point = 1 / 30.0
+ classic_use_bonus_ues = True
+
+ def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
+ """calcul du bonus"""
+ # Prend la note de chaque modimpl, sans considération d'UE
+ if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
+ sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
+ # ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
+ note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
+ ues = self.formsemestre.query_ues(with_sport=False).all()
+ ues_idx = [ue.id for ue in ues]
+
+ if self.formsemestre.formation.is_apc(): # --- BUT
+ bonus_moy_arr = np.where(
+ note_bonus_max > self.seuil_moy_gen,
+ (note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
+ 0.0,
+ )
+ self.bonus_ues = pd.DataFrame(
+ np.stack([bonus_moy_arr] * len(ues)).T,
+ index=self.etuds_idx,
+ columns=ues_idx,
+ dtype=float,
+ )
+
+
class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours.
diff --git a/app/models/groups.py b/app/models/groups.py
index 976d465b..f6452cf7 100644
--- a/app/models/groups.py
+++ b/app/models/groups.py
@@ -63,6 +63,12 @@ class GroupDescr(db.Model):
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
+ etuds = db.relationship(
+ "Identite",
+ secondary="group_membership",
+ lazy="dynamic",
+ )
+
def __repr__(self):
return (
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
diff --git a/app/scodoc/sco_bulletins_generator.py b/app/scodoc/sco_bulletins_generator.py
index 2aeb792f..5fa07aa8 100644
--- a/app/scodoc/sco_bulletins_generator.py
+++ b/app/scodoc/sco_bulletins_generator.py
@@ -71,6 +71,7 @@ class BulletinGenerator:
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
description = "superclass for bulletins" # description for user interface
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
+ scale_table_in_page = True # rescale la table sur 1 page
def __init__(
self,
@@ -163,8 +164,9 @@ class BulletinGenerator:
# signatures
objects += self.bul_signatures_pdf() # pylint: disable=no-member
- # Réduit sur une page
- objects = [KeepInFrame(0, 0, objects, mode="shrink")]
+ if self.scale_table_in_page:
+ # Réduit sur une page
+ objects = [KeepInFrame(0, 0, objects, mode="shrink")]
#
if not stand_alone:
objects.append(PageBreak()) # insert page break at end
@@ -219,7 +221,7 @@ class BulletinGenerator:
# ---------------------------------------------------------------------------
def make_formsemestre_bulletinetud(
infos,
- version="long", # short, long, selectedevals
+ version=None, # short, long, selectedevals
format="pdf", # html, pdf
stand_alone=True,
):
@@ -231,6 +233,7 @@ def make_formsemestre_bulletinetud(
"""
from app.scodoc import sco_preferences
+ version = version or "long"
if not version in scu.BULLETINS_VERSIONS:
raise ValueError("invalid version code !")
diff --git a/app/scodoc/sco_bulletins_standard.py b/app/scodoc/sco_bulletins_standard.py
index fd84e7d9..95f6f172 100644
--- a/app/scodoc/sco_bulletins_standard.py
+++ b/app/scodoc/sco_bulletins_standard.py
@@ -46,10 +46,11 @@ de la forme %(XXX)s sont remplacées par la valeur de XXX, pour XXX dans:
Balises img: actuellement interdites.
"""
+from reportlab.platypus import KeepTogether, Paragraph, Spacer, Table
+from reportlab.lib.units import cm, mm
+from reportlab.lib.colors import Color, blue
import app.scodoc.sco_utils as scu
-from app.scodoc.sco_pdf import Color, Paragraph, Spacer, Table
-from app.scodoc.sco_pdf import blue, cm, mm
from app.scodoc.sco_pdf import SU
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
@@ -195,7 +196,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
# -----
if format == "pdf":
- return Op
+ return [KeepTogether(Op)]
elif format == "html":
return "\n".join(H)
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index 2a27b498..48a1dbba 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -124,7 +124,7 @@ def get_partition(partition_id):
{"partition_id": partition_id},
)
if not r:
- raise ValueError("invalid partition_id (%s)" % partition_id)
+ raise ScoValueError(f"Partition inconnue (déjà supprimée ?) ({partition_id})")
return r[0]
diff --git a/scodoc.py b/scodoc.py
index 9eea0f1c..c728bed9 100755
--- a/scodoc.py
+++ b/scodoc.py
@@ -14,6 +14,7 @@ import click
import flask
from flask.cli import with_appcontext
from flask.templating import render_template
+from flask_login import login_user, logout_user, current_user
import psycopg2
import sqlalchemy
@@ -45,7 +46,6 @@ cli.register(app)
def make_shell_context():
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
- from flask_login import login_user, logout_user, current_user
import app as mapp # le package app
import numpy as np
import pandas as pd
@@ -504,6 +504,8 @@ def init_test_database():
ctx = app.test_request_context()
ctx.push()
+ admin = User.query.filter_by(user_name="admin").first()
+ login_user(admin)
create_test_api_database.init_test_database()
diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py
index bfd346fa..cd5a7e7e 100644
--- a/tools/fakedatabase/create_test_api_database.py
+++ b/tools/fakedatabase/create_test_api_database.py
@@ -30,7 +30,12 @@ from flask_login import login_user
from app import auth
from app import models
from app import db
-from app.scodoc import sco_formations
+from app.scodoc import (
+ sco_formations,
+ sco_formsemestre,
+ sco_formsemestre_inscriptions,
+ sco_groups,
+)
from tools.fakeportal.gen_nomprenoms import nomprenom
# La formation à utiliser:
@@ -69,19 +74,26 @@ def create_user(dept):
return user
-def create_fake_etud():
+def create_fake_etud(dept):
"""Créé un faux étudiant et l'insère dans la base"""
civilite = random.choice(("M", "F", "X"))
nom, prenom = nomprenom(civilite)
- etud = models.Identite(civilite=civilite, nom=nom, prenom=prenom)
+ etud = models.Identite(civilite=civilite, nom=nom, prenom=prenom, dept_id=dept.id)
db.session.add(etud)
db.session.commit()
+ adresse = models.Adresse(
+ etudid=etud.id, email=f"{etud.prenom}.{etud.nom}@example.com"
+ )
+ db.session.add(adresse)
+ admission = models.Admission(etudid=etud.id)
+ db.session.add(admission)
+ db.session.commit()
return etud
-def create_etuds(nb=16):
+def create_etuds(dept, nb=16):
"create nb etuds"
- return [create_fake_etud() for _ in range(nb)]
+ return [create_fake_etud(dept) for _ in range(nb)]
def create_formsemestre(formation, user, semestre_idx=1):
@@ -104,30 +116,29 @@ def create_formsemestre(formation, user, semestre_idx=1):
)
db.session.add(modimpl)
db.session.commit()
+ partition_id = sco_groups.partition_create(
+ formsemestre.id, default=True, redirect=False
+ )
+ _group_id = sco_groups.create_group(partition_id, default=True)
return formsemestre
def inscrit_etudiants(etuds, formsemestre):
"""Inscrit les etudiants aux semestres et à tous ses modules"""
for etud in etuds:
- ins = models.FormSemestreInscription(
- etudid=etud.id, formsemestre_id=formsemestre.id, etat="I"
+ sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
+ formsemestre.id,
+ etud.id,
+ group_ids=[],
+ etat="I",
+ method="init db test",
)
- db.session.add(ins)
- for modimpl in formsemestre.modimpls:
- insmod = models.ModuleImplInscription(
- etudid=etud.id, moduleimpl_id=modimpl.id
- )
- db.session.add(insmod)
- db.session.commit()
def init_test_database():
dept = init_departement("TAPI")
user = create_user(dept)
- login_user(user)
-
- etuds = create_etuds()
+ etuds = create_etuds(dept)
formation = import_formation()
formsemestre = create_formsemestre(formation, user)
inscrit_etudiants(etuds, formsemestre)