From c488ad3a6262351ec539c81d66641b5f189a4a0a Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Wed, 6 Apr 2022 18:51:01 +0200
Subject: [PATCH 1/8] Nettoyage code table recap.
---
app/comp/res_common.py | 23 +-
app/scodoc/gen_tables.py | 36 +-
app/scodoc/sco_archives.py | 63 +-
app/scodoc/sco_excel.py | 9 +-
app/scodoc/sco_formsemestre_validation.py | 11 -
app/scodoc/sco_preferences.py | 12 -
app/scodoc/sco_recapcomplet.py | 836 +++-------------------
sco_version.py | 2 +-
8 files changed, 192 insertions(+), 800 deletions(-)
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index 6e37a8c2d..d7240598a 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -542,17 +542,18 @@ class ResultatsSemestre(ResultatsCache):
# Le bonus sport appliqué sur cette UE
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
val = self.bonus_ues[ue.id][etud.id] or ""
- val_fmt = fmt_note(val)
+ val_fmt = val_fmt_html = fmt_note(val)
if val:
- val_fmt = f'{val_fmt}'
+ val_fmt_html = f'{val_fmt}'
idx = add_cell(
row,
f"bonus_ue_{ue.id}",
f"Bonus {ue.acronyme}",
- val_fmt,
+ val_fmt_html,
"col_ue_bonus",
idx,
)
+ row[f"_bonus_ue_{ue.id}_xls"] = val_fmt
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
idx_malus = idx # place pour colonne malus à gauche des modules
idx += 1
@@ -581,20 +582,21 @@ class ResultatsSemestre(ResultatsCache):
col_id = (
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
)
- val_fmt = fmt_note(val)
+ val_fmt = val_fmt_html = fmt_note(val)
if modimpl.module.module_type == scu.ModuleType.MALUS:
- val_fmt = (
+ val_fmt_html = (
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
)
idx = add_cell(
row,
col_id,
modimpl.module.code,
- val_fmt,
+ val_fmt_html,
# class col_res mod_ue_123
f"col_{modimpl.module.type_abbrv()} mod_ue_{ue.id}",
idx,
)
+ row[f"_{col_id}_xls"] = val_fmt
if modimpl.module.module_type == scu.ModuleType.MALUS:
titles[f"_{col_id}_col_order"] = idx_malus
titles_bot[f"_{col_id}_target"] = url_for(
@@ -611,17 +613,20 @@ class ResultatsSemestre(ResultatsCache):
f"_{col_id}_target_attrs"
] = f""" title="{modimpl.module.titre} ({nom_resp})" """
modimpl_ids.add(modimpl.id)
- ue_valid_txt = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
+ ue_valid_txt = (
+ ue_valid_txt_html
+ ) = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
if nb_ues_warning:
- ue_valid_txt += " " + scu.EMO_WARNING
+ ue_valid_txt_html += " " + scu.EMO_WARNING
add_cell(
row,
"ues_validables",
"UEs",
- ue_valid_txt,
+ ue_valid_txt_html,
"col_ue col_ues_validables",
29, # juste avant moy. gen.
)
+ row["_ues_validables_xls"] = ue_valid_txt
if nb_ues_warning:
row["_ues_validables_class"] += " moy_ue_warning"
elif nb_ues_validables < len(ues_sans_bonus):
diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py
index 7f5531c6f..2136ee841 100644
--- a/app/scodoc/gen_tables.py
+++ b/app/scodoc/gen_tables.py
@@ -209,7 +209,8 @@ class GenTable(object):
omit_hidden_lines=False,
pdf_mode=False, # apply special pdf reportlab processing
pdf_style_list=[], # modified: list of platypus table style commands
- ):
+ xls_mode=False, # get xls content if available
+ ) -> list:
"table data as a list of lists (rows)"
T = []
line_num = 0 # line number in input data
@@ -237,9 +238,14 @@ class GenTable(object):
# if colspan_count > 0:
# continue # skip cells after a span
if pdf_mode:
- content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
+ content = row.get(f"_{cid}_pdf", False) or row.get(cid, "")
+ elif xls_mode:
+ content = row.get(f"_{cid}_xls", False) or row.get(cid, "")
else:
- content = row.get(cid, "") or "" # nota: None converted to ''
+ content = row.get(cid, "")
+ # Convert None to empty string ""
+ content = "" if content is None else content
+
colspan = row.get("_%s_colspan" % cid, 0)
if colspan > 1:
pdf_style_list.append(
@@ -299,7 +305,7 @@ class GenTable(object):
return self.xml()
elif format == "json":
return self.json()
- raise ValueError("GenTable: invalid format: %s" % format)
+ raise ValueError(f"GenTable: invalid format: {format}")
def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
"row is a dict, returns a string ..."
@@ -479,23 +485,23 @@ class GenTable(object):
def excel(self, wb=None):
"""Simple Excel representation of the table"""
if wb is None:
- ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
+ sheet = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
else:
- ses = wb.create_sheet(sheet_name=self.xls_sheet_name)
- ses.rows += self.xls_before_table
+ sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
+ sheet.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True)
style_base = sco_excel.excel_make_style()
- ses.append_row(ses.make_row(self.get_titles_list(), style_bold))
- for line in self.get_data_list():
- ses.append_row(ses.make_row(line, style_base))
+ sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
+ for line in self.get_data_list(xls_mode=True):
+ sheet.append_row(sheet.make_row(line, style_base))
if self.caption:
- ses.append_blank_row() # empty line
- ses.append_single_cell_row(self.caption, style_base)
+ sheet.append_blank_row() # empty line
+ sheet.append_single_cell_row(self.caption, style_base)
if self.origin:
- ses.append_blank_row() # empty line
- ses.append_single_cell_row(self.origin, style_base)
+ sheet.append_blank_row() # empty line
+ sheet.append_single_cell_row(self.origin, style_base)
if wb is None:
- return ses.generate()
+ return sheet.generate()
def text(self):
"raw text representation of the table"
diff --git a/app/scodoc/sco_archives.py b/app/scodoc/sco_archives.py
index 0103caa24..773fc0426 100644
--- a/app/scodoc/sco_archives.py
+++ b/app/scodoc/sco_archives.py
@@ -47,14 +47,15 @@
qui est une description (humaine, format libre) de l'archive.
"""
-import chardet
import datetime
import glob
+import json
import mimetypes
import os
import re
import shutil
import time
+import chardet
import flask
from flask import g, request
@@ -63,7 +64,9 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu
from config import Config
from app import log
-from app.models import Departement
+from app.comp import res_sem
+from app.comp.res_compat import NotesTableCompat
+from app.models import Departement, FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import (
AccessDenied,
@@ -95,8 +98,8 @@ class BaseArchiver(object):
self.root = os.path.join(*dirs)
log("initialized archiver, path=" + self.root)
path = dirs[0]
- for dir in dirs[1:]:
- path = os.path.join(path, dir)
+ for directory in dirs[1:]:
+ path = os.path.join(path, directory)
try:
scu.GSL.acquire()
if not os.path.isdir(path):
@@ -117,11 +120,11 @@ class BaseArchiver(object):
try:
scu.GSL.acquire()
if not os.path.isdir(dept_dir):
- log("creating directory %s" % dept_dir)
+ log(f"creating directory {dept_dir}")
os.mkdir(dept_dir)
obj_dir = os.path.join(dept_dir, str(oid))
if not os.path.isdir(obj_dir):
- log("creating directory %s" % obj_dir)
+ log(f"creating directory {obj_dir}")
os.mkdir(obj_dir)
finally:
scu.GSL.release()
@@ -163,8 +166,9 @@ class BaseArchiver(object):
def get_archive_date(self, archive_id):
"""Returns date (as a DateTime object) of an archive"""
- dt = [int(x) for x in os.path.split(archive_id)[1].split("-")]
- return datetime.datetime(*dt)
+ return datetime.datetime(
+ *[int(x) for x in os.path.split(archive_id)[1].split("-")]
+ )
def list_archive(self, archive_id: str) -> str:
"""Return list of filenames (without path) in archive"""
@@ -195,8 +199,7 @@ class BaseArchiver(object):
archive_id = os.path.join(self.get_obj_dir(oid), archive_name)
if not os.path.isdir(archive_id):
log(
- "invalid archive name: %s, oid=%s, archive_id=%s"
- % (archive_name, oid, archive_id)
+ f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}"
)
raise ValueError("invalid archive name")
return archive_id
@@ -223,7 +226,7 @@ class BaseArchiver(object):
+ os.path.sep
+ "-".join(["%02d" % x for x in time.localtime()[:6]])
)
- log("creating archive: %s" % archive_id)
+ log(f"creating archive: {archive_id}")
try:
scu.GSL.acquire()
os.mkdir(archive_id) # if exists, raises an OSError
@@ -302,9 +305,14 @@ def do_formsemestre_archive(
Store:
- tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf)
"""
- from app.scodoc.sco_recapcomplet import make_formsemestre_recapcomplet
+ from app.scodoc.sco_recapcomplet import (
+ gen_formsemestre_recapcomplet_excel,
+ gen_formsemestre_recapcomplet_html,
+ gen_formsemestre_recapcomplet_json,
+ )
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id
archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
@@ -319,37 +327,38 @@ def do_formsemestre_archive(
etudids = [m["etudid"] for m in groups_infos.members]
# Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes)
- data, _, _ = make_formsemestre_recapcomplet(formsemestre_id, format="xls")
+ data, _ = gen_formsemestre_recapcomplet_excel(
+ formsemestre, res, include_evaluations=True, format="xls"
+ )
if data:
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
- data, _, _ = make_formsemestre_recapcomplet(
- formsemestre_id, format="html", disable_etudlink=True
+ table_html = gen_formsemestre_recapcomplet_html(
+ formsemestre, res, include_evaluations=True
)
- if data:
+ if table_html:
data = "\n".join(
[
html_sco_header.sco_header(
- page_title="Moyennes archivées le %s" % date,
- head_message="Moyennes archivées le %s" % date,
+ page_title=f"Moyennes archivées le {date}",
+ head_message=f"Moyennes archivées le {date}",
no_side_bar=True,
),
- '
Valeurs archivées le %s
' % date,
+ f'
Valeurs archivées le {date}
',
'',
- data,
+ table_html,
html_sco_header.sco_footer(),
]
)
data = data.encode(scu.SCO_ENCODING)
PVArchive.store(archive_id, "Tableau_moyennes.html", data)
- # Bulletins en XML (pour tous les etudiants, n'utilise pas les groupes)
- data, _, _ = make_formsemestre_recapcomplet(
- formsemestre_id, format="xml", xml_with_decisions=True
- )
+ # Bulletins en JSON
+ data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
+ data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
+ data_js = data_js.encode(scu.SCO_ENCODING)
if data:
- data = data.encode(scu.SCO_ENCODING)
- PVArchive.store(archive_id, "Bulletins.xml", data)
+ PVArchive.store(archive_id, "Bulletins.json", data_js)
# Decisions de jury, en XLS
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
if data:
diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 978247800..5d1b2d729 100644
--- a/app/scodoc/sco_excel.py
+++ b/app/scodoc/sco_excel.py
@@ -185,13 +185,13 @@ def excel_make_style(
class ScoExcelSheet:
- """Représente une feuille qui peut être indépendante ou intégrée dans un SCoExcelBook.
+ """Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations
est imposé:
* instructions globales (largeur/maquage des colonnes et ligne, ...)
* construction et ajout des cellules et ligne selon le sens de lecture (occidental)
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
- * pour finit appel de la méthode de génération
+ * pour finir appel de la méthode de génération
"""
def __init__(self, sheet_name="feuille", default_style=None, wb=None):
@@ -260,7 +260,7 @@ class ScoExcelSheet:
for i, val in enumerate(value):
self.ws.column_dimensions[self.i2col(i)].width = val
# No keys: value is a list of widths
- elif type(cle) == str: # accepts set_column_with("D", ...)
+ elif isinstance(cle, str): # accepts set_column_with("D", ...)
self.ws.column_dimensions[cle].width = value
else:
self.ws.column_dimensions[self.i2col(cle)].width = value
@@ -337,7 +337,8 @@ class ScoExcelSheet:
return cell
- def make_row(self, values: list, style=None, comments=None):
+ def make_row(self, values: list, style=None, comments=None) -> list:
+ "build a row"
# TODO make possible differents styles in a row
if comments is None:
comments = [None] * len(values)
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index 96d0a737c..ba517e6ae 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -184,9 +184,6 @@ def formsemestre_validation_etud_form(
"notes.formsemestre_recapcomplet",
scodoc_dept=g.scodoc_dept,
modejury=1,
- hidemodules=1,
- hidebac=1,
- pref_override=0,
formsemestre_id=formsemestre_id,
sortcol=sortcol
or None, # pour refaire tri sorttable du tableau de notes
@@ -437,14 +434,6 @@ def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol
# sinon renvoie au listing general,
-# if choice.new_code_prev:
-# flask.redirect( 'formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1&desturl=%s' % (formsemestre_id, etudid, desturl) )
-# else:
-# if not desturl:
-# desturl = 'formsemestre_recapcomplet?modejury=1&hidemodules=1&formsemestre_id=' + str(formsemestre_id)
-# flask.redirect(desturl)
-
-
def _dispcode(c):
if not c:
return ""
diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index 7f1d8cc41..112b0da50 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -382,18 +382,6 @@ class BasePreferences(object):
"only_global": False,
},
),
- (
- "recap_hidebac",
- {
- "initvalue": 0,
- "title": "Cacher la colonne Bac",
- "explanation": "sur la table récapitulative",
- "input_type": "boolcheckbox",
- "category": "misc",
- "labels": ["non", "oui"],
- "only_global": False,
- },
- ),
# ------------------ Absences
(
"email_chefdpt",
diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py
index 84fbafc38..72d16012c 100644
--- a/app/scodoc/sco_recapcomplet.py
+++ b/app/scodoc/sco_recapcomplet.py
@@ -28,7 +28,6 @@
"""Tableau récapitulatif des notes d'un semestre
"""
import datetime
-import json
import time
from xml.etree import ElementTree
@@ -43,6 +42,7 @@ from app.models import FormSemestre
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
+from app.scodoc.gen_tables import GenTable
import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_json
@@ -52,6 +52,7 @@ from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
+from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status
@@ -67,42 +68,38 @@ from app.scodoc.sco_codes_parcours import DEF, UE_SPORT
def formsemestre_recapcomplet(
formsemestre_id=None,
modejury=False, # affiche lien saisie decision jury
- hidemodules=False, # cache colonnes notes modules
- hidebac=False, # cache colonne Bac
tabformat="html",
sortcol=None,
xml_with_decisions=False, # XML avec decisions
rank_partition_id=None, # si None, calcul rang global
- pref_override=True, # si vrai, les prefs ont la priorite sur le param hidebac
force_publishing=True, # publie les XML/JSON meme si bulletins non publiés
):
"""Page récapitulant les notes d'un semestre.
Grand tableau récapitulatif avec toutes les notes de modules
pour tous les étudiants, les moyennes par UE et générale,
trié par moyenne générale décroissante.
+
+ tabformat:
+ html : page web
+ evals : page web, avec toutes les évaluations dans le tableau
+ xls, xlsx: export excel simple
+ xlsall : export excel simple, avec toutes les évaluations dans le tableau
+ csv : export CSV, avec toutes les évaluations
+ xml, json : concaténation de tous les bulletins, au format demandé
+ pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable)
+
+ modejury: cache modules, affiche lien saisie decision jury
+
"""
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
- parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- # Pour APC (BUT): cache les modules par défaut car moyenne n'a pas de sens
- if formsemestre.formation.is_apc():
- hidemodules = True
- # traduit du DTML
+
modejury = int(modejury)
- hidemodules = (
- int(hidemodules) or parcours.UE_IS_MODULE
- ) # cache les colonnes des modules
- pref_override = int(pref_override)
- if pref_override:
- hidebac = int(sco_preferences.get_preference("recap_hidebac", formsemestre_id))
- else:
- hidebac = int(hidebac)
+
xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing)
- isFile = tabformat in ("csv", "xls", "xml", "xlsall", "json")
+ is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"}
H = []
- if not isFile:
+ if not is_file:
H += [
html_sco_header.sco_header(
page_title="Récapitulatif",
@@ -115,17 +112,14 @@ def formsemestre_recapcomplet(
),
]
if len(formsemestre.inscriptions) > 0:
- H += [
- '")
H.append(
- """
"""
)
else:
H.append(
- """Saisie des décisions du jury"""
- % formsemestre_id
+ f"""Saisie des décisions du jury"""
)
H.append("")
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
@@ -219,684 +214,59 @@ def do_formsemestre_recapcomplet(
):
"""Calcule et renvoie le tableau récapitulatif."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+
+ filename = scu.sanitize_filename(
+ f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
+ )
+
if (format == "html" or format == "evals") and not modejury:
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- data, filename = gen_formsemestre_recapcomplet_html(
- formsemestre, res, include_evaluations=(format == "evals")
+ data = gen_formsemestre_recapcomplet_html(
+ formsemestre,
+ res,
+ include_evaluations=(format == "evals"),
+ filename=filename,
)
- else:
- data, filename, format = make_formsemestre_recapcomplet(
- formsemestre_id=formsemestre_id,
- format=format,
- hidemodules=hidemodules,
- hidebac=hidebac,
- xml_nodate=xml_nodate,
- modejury=modejury,
- sortcol=sortcol,
- xml_with_decisions=xml_with_decisions,
- disable_etudlink=disable_etudlink,
- rank_partition_id=rank_partition_id,
- force_publishing=force_publishing,
- )
- # ---
- if format == "xml" or format == "html" or format == "evals":
return data
- elif format == "csv":
- return scu.send_file(data, filename=filename, mime=scu.CSV_MIMETYPE)
- elif format.startswith("xls") or format.startswith("xlsx"):
- return scu.send_file(data, filename=filename, mime=scu.XLSX_MIMETYPE)
- elif format == "json":
- js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
- return scu.send_file(
- js, filename=filename, suffix=scu.JSON_SUFFIX, mime=scu.JSON_MIMETYPE
+ elif format.startswith("xls") or format == "csv":
+ res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
+ include_evaluations = format in {"xlsall", "csv "}
+ if format != "csv":
+ format = "xlsx"
+ data, filename = gen_formsemestre_recapcomplet_excel(
+ formsemestre,
+ res,
+ include_evaluations=include_evaluations,
+ format=format,
+ filename=filename,
)
- else:
- raise ValueError(f"unknown format {format}")
-
-
-def make_formsemestre_recapcomplet(
- formsemestre_id=None,
- format="html", # html, evals, xml, json
- hidemodules=False, # ne pas montrer les modules (ignoré en XML)
- hidebac=False, # pas de colonne Bac (ignoré en XML)
- xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
- modejury=False, # saisie décisions jury
- sortcol=None, # indice colonne a trier dans table T
- xml_with_decisions=False,
- disable_etudlink=False,
- rank_partition_id=None, # si None, calcul rang global
- force_publishing=True, # donne bulletins JSON/XML meme si non publiés
-):
- """Grand tableau récapitulatif avec toutes les notes de modules
- pour tous les étudiants, les moyennes par UE et générale,
- trié par moyenne générale décroissante.
- """
- civ_nom_prenom = False # 3 colonnes différentes ou une seule avec prénom abrégé ?
- if format == "xml":
- return _formsemestre_recapcomplet_xml(
+ return scu.send_file(data, filename=filename, mime=scu.get_mime_suffix(format))
+ elif format == "xml":
+ data = gen_formsemestre_recapcomplet_xml(
formsemestre_id,
xml_nodate,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
)
+ return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX)
elif format == "json":
- return _formsemestre_recapcomplet_json(
+ data = gen_formsemestre_recapcomplet_json(
formsemestre_id,
xml_nodate=xml_nodate,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
)
- if format[:3] == "xls":
- civ_nom_prenom = True # 3 cols: civilite, nom, prenom
- keep_numeric = True # pas de conversion des notes en strings
- else:
- keep_numeric = False
+ return scu.sendJSON(data, filename=filename)
- if hidebac:
- admission_extra_cols = []
- else:
- admission_extra_cols = [
- "type_admission",
- "classement",
- "apb_groupe",
- "apb_classement_gr",
- ]
-
- formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- # A ré-écrire XXX
- sem = sco_formsemestre.do_formsemestre_list(
- args={"formsemestre_id": formsemestre_id}
- )[0]
- parcours = formsemestre.formation.get_parcours()
-
- nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
- modimpls = formsemestre.modimpls_sorted
- ues = nt.get_ues_stat_dict() # incluant le(s) UE de sport
-
- partitions, partitions_etud_groups = sco_groups.get_formsemestre_groups(
- formsemestre_id
- )
- if rank_partition_id and format == "html":
- # Calcul rang sur une partition et non sur l'ensemble
- # seulement en format HTML (car colonnes rangs toujours presentes en xls)
- rank_partition = sco_groups.get_partition(rank_partition_id)
- rank_label = "Rg (%s)" % rank_partition["partition_name"]
- else:
- rank_partition = sco_groups.get_default_partition(formsemestre_id)
- rank_label = "Rg"
-
- T = nt.get_table_moyennes_triees()
- if not T:
- return "", "", format
-
- # Construit une liste de listes de chaines: le champs du tableau resultat (HTML ou CSV)
- F = []
- h = [rank_label]
- if civ_nom_prenom:
- h += ["Civilité", "Nom", "Prénom"]
- else:
- h += ["Nom"]
- if not hidebac:
- h.append("Bac")
-
- # Si CSV ou XLS, indique tous les groupes
- if format[:3] == "xls" or format == "csv":
- for partition in partitions:
- h.append("%s" % partition["partition_name"])
- else:
- h.append("Gr")
-
- h.append("Moy")
- # Ajoute rangs dans groupe seulement si CSV ou XLS
- if format[:3] == "xls" or format == "csv":
- for partition in partitions:
- h.append("rang_%s" % partition["partition_name"])
-
- cod2mod = {} # code : moduleimpl
- mod_evals = {} # moduleimpl_id : liste de toutes les evals de ce module
- for ue in ues:
- if ue["type"] != UE_SPORT:
- h.append(ue["acronyme"])
- else: # UE_SPORT:
- # n'affiche pas la moyenne d'UE dans ce cas
- # mais laisse col. vide si modules affichés (pour séparer les UE)
- if not hidemodules:
- h.append("")
- pass
- if not hidemodules and not ue["is_external"]:
- for modimpl in modimpls:
- if modimpl.module.ue_id == ue["ue_id"]:
- code = modimpl.module.code
- h.append(code)
- cod2mod[code] = modimpl # pour fabriquer le lien
- if format == "xlsall":
- evals = nt.modimpls_results[
- modimpl.id
- ].get_evaluations_completes(modimpl)
- # evals = nt.get_mod_evaluation_etat_list(...
- mod_evals[modimpl.id] = evals
- h += _list_notes_evals_titles(code, evals)
-
- h += admission_extra_cols
- h += ["code_nip", "etudid"]
- F.append(h)
-
- def fmtnum(val): # conversion en nombre pour cellules excel
- if keep_numeric:
- try:
- return float(val)
- except:
- return val
- else:
- return val
-
- # Compte les decisions de jury
- codes_nb = scu.DictDefault(defaultvalue=0)
- #
- is_dem = {} # etudid : bool
- for t in T:
- etudid = t[-1]
- dec = nt.get_etud_decision_sem(etudid)
- if dec:
- codes_nb[dec["code"]] += 1
- etud_etat = nt.get_etud_etat(etudid)
- if etud_etat == "D":
- gr_name = "Dém."
- is_dem[etudid] = True
- elif etud_etat == DEF:
- gr_name = "Déf."
- is_dem[etudid] = False
- else:
- group = sco_groups.get_etud_main_group(etudid, formsemestre_id)
- gr_name = group["group_name"] or ""
- is_dem[etudid] = False
- if rank_partition_id:
- rang_gr, _, rank_gr_name = sco_bulletins.get_etud_rangs_groups(
- etudid, formsemestre_id, partitions, partitions_etud_groups, nt
- )
- if rank_gr_name[rank_partition_id]:
- rank = "%s %s" % (
- rank_gr_name[rank_partition_id],
- rang_gr[rank_partition_id],
- )
- else:
- rank = ""
- else:
- rank = nt.get_etud_rang(etudid)
-
- e = nt.identdict[etudid]
- if civ_nom_prenom:
- sco_etud.format_etud_ident(e)
- l = [rank, e["civilite_str"], e["nom_disp"], e["prenom"]] # civ, nom prenom
- else:
- l = [rank, nt.get_nom_short(etudid)] # rang, nom,
-
- e["admission"] = {}
- if not hidebac:
- e["admission"] = nt.etuds_dict[etudid].admission.first()
- if e["admission"]:
- bac = nt.etuds_dict[etudid].admission[0].get_bac()
- l.append(bac.abbrev())
- else:
- l.append("")
-
- if format[:3] == "xls" or format == "csv": # tous les groupes
- for partition in partitions:
- group = partitions_etud_groups[partition["partition_id"]].get(
- etudid, None
- )
- if group:
- l.append(group["group_name"])
- else:
- l.append("")
- else:
- l.append(gr_name) # groupe
-
- # Moyenne générale
- l.append(fmtnum(scu.fmt_note(t[0], keep_numeric=keep_numeric)))
- # Ajoute rangs dans groupes seulement si CSV ou XLS
- if format[:3] == "xls" or format == "csv":
- rang_gr, _, gr_name = sco_bulletins.get_etud_rangs_groups(
- etudid, formsemestre_id, partitions, partitions_etud_groups, nt
- )
-
- for partition in partitions:
- l.append(rang_gr[partition["partition_id"]])
-
- # Nombre d'UE au dessus de 10
- # t[i] est une chaine :-)
- # nb_ue_ok = sum(
- # [t[i] > 10 for i, ue in enumerate(ues, start=1) if ue["type"] != UE_SPORT]
- # )
- ue_index = [] # indices des moy UE dans l (pour appliquer style css)
- for i, ue in enumerate(ues, start=1):
- if ue["type"] != UE_SPORT:
- l.append(
- fmtnum(scu.fmt_note(t[i], keep_numeric=keep_numeric))
- ) # moyenne etud dans ue
- else: # UE_SPORT:
- # n'affiche pas la moyenne d'UE dans ce cas
- if not hidemodules:
- l.append("")
- ue_index.append(len(l) - 1)
- if not hidemodules and not ue["is_external"]:
- j = 0
- for modimpl in modimpls:
- if modimpl.module.ue_id == ue["ue_id"]:
- l.append(
- fmtnum(
- scu.fmt_note(
- t[j + len(ues) + 1], keep_numeric=keep_numeric
- )
- )
- ) # moyenne etud dans module
- if format == "xlsall":
- l += _list_notes_evals(mod_evals[modimpl.id], etudid)
- j += 1
- if not hidebac:
- for k in admission_extra_cols:
- l.append(getattr(e["admission"], k, "") or "")
- l.append(
- nt.identdict[etudid]["code_nip"] or ""
- ) # avant-derniere colonne = code_nip
- l.append(etudid) # derniere colonne = etudid
- F.append(l)
-
- # Dernière ligne: moyennes, min et max des UEs et modules
- if not hidemodules: # moy/min/max dans chaque module
- mods_stats = {} # moduleimpl_id : stats
- for modimpl in modimpls:
- mods_stats[modimpl.id] = nt.get_mod_stats(modimpl.id)
-
- def add_bottom_stat(key, title, corner_value=""):
- l = ["", title]
- if civ_nom_prenom:
- l += ["", ""]
- if not hidebac:
- l.append("")
- if format[:3] == "xls" or format == "csv":
- l += [""] * len(partitions)
- else:
- l += [""]
- l.append(corner_value)
- if format[:3] == "xls" or format == "csv":
- for _ in partitions:
- l += [""] # rangs dans les groupes
- for ue in ues:
- if ue["type"] != UE_SPORT:
- if key == "nb_valid_evals":
- l.append("")
- elif key == "coef":
- if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
- l.append("%2.3f" % ue["coefficient"])
- else:
- l.append("")
- else:
- if key == "ects":
- if keep_numeric:
- l.append(ue[key])
- else:
- l.append(str(ue[key]))
- else:
- l.append(scu.fmt_note(ue[key], keep_numeric=keep_numeric))
- else: # UE_SPORT:
- # n'affiche pas la moyenne d'UE dans ce cas
- if not hidemodules:
- l.append("")
- # ue_index.append(len(l) - 1)
- if not hidemodules and not ue["is_external"]:
- for modimpl in modimpls:
- if modimpl.module.ue_id == ue["ue_id"]:
- if key == "coef":
- coef = modimpl.module.coefficient
- if format[:3] != "xls":
- coef = str(coef)
- l.append(coef)
- elif key == "ects":
- l.append("") # ECTS module ?
- else:
- val = mods_stats[modimpl.id][key]
- if key == "nb_valid_evals":
- if (
- format[:3] != "xls"
- ): # garde val numerique pour excel
- val = str(val)
- else: # moyenne du module
- val = scu.fmt_note(val, keep_numeric=keep_numeric)
- l.append(val)
-
- if format == "xlsall":
- l += _list_notes_evals_stats(mod_evals[modimpl.id], key)
- if modejury:
- l.append("") # case vide sur ligne "Moyennes"
-
- l += [""] * len(admission_extra_cols) # infos admission vides ici
- F.append(l + ["", ""]) # ajoute cellules code_nip et etudid inutilisees ici
-
- add_bottom_stat(
- "min", "Min", corner_value=scu.fmt_note(nt.moy_min, keep_numeric=keep_numeric)
- )
- add_bottom_stat(
- "max", "Max", corner_value=scu.fmt_note(nt.moy_max, keep_numeric=keep_numeric)
- )
- add_bottom_stat(
- "moy",
- "Moyennes",
- corner_value=scu.fmt_note(nt.moy_moy, keep_numeric=keep_numeric),
- )
- add_bottom_stat("coef", "Coef")
- add_bottom_stat("nb_valid_evals", "Nb évals")
- add_bottom_stat("ects", "ECTS")
-
- # Génération de la table au format demandé
- if format == "html":
- # Table format HTML
- H = [
- """
-
-
- """
- ]
- if sortcol: # sort table using JS sorttable
- H.append(
- """
- """
- % (int(sortcol))
- )
-
- ligne_titres_head = _ligne_titres(
- ue_index, F, cod2mod, modejury, with_modules_links=False
- )
- ligne_titres_foot = _ligne_titres(
- ue_index, F, cod2mod, modejury, with_modules_links=True
- )
-
- H.append("\n" + ligne_titres_head + "\n\n\n")
- if disable_etudlink:
- etudlink = "%(name)s"
- else:
- etudlink = """%(name)s"""
- ir = 0
- nblines = len(F) - 1
- for l in F[1:]:
- etudid = l[-1]
- if ir == nblines - 6:
- H.append("")
- H.append("")
- if ir >= nblines - 6:
- # dernieres lignes:
- el = l[1]
- styl = (
- "recap_row_min",
- "recap_row_max",
- "recap_row_moy",
- "recap_row_coef",
- "recap_row_nbeval",
- "recap_row_ects",
- )[ir - nblines + 6]
- cells = f'
'
- else:
- el = etudlink % {
- "formsemestre_id": formsemestre_id,
- "etudid": etudid,
- "name": l[1],
- }
- if ir % 2 == 0:
- cells = f'
'
- else:
- cells = f'
'
- ir += 1
- # XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ]
- # notes sans le NA:
- nsn = l[:-2] # copy
- for i, _ in enumerate(nsn):
- if nsn[i] == "NA":
- nsn[i] = "-"
- try:
- order = int(nsn[0].split()[0])
- except:
- order = 99999
- cells += (
- f'
{nsn[0]}
' # rang
- )
- cells += '
%s
' % el # nom etud (lien)
- if not hidebac:
- cells += '
")
-
- # Form pour choisir partition de classement:
- if not modejury and partitions:
- H.append("Afficher le rang des groupes de: ")
- if not rank_partition_id:
- checked = "checked"
- else:
- checked = ""
- H.append(
- 'tous '
- % (checked)
- )
- for p in partitions:
- if p["partition_id"] == rank_partition_id:
- checked = "checked"
- else:
- checked = ""
- H.append(
- '%s '
- % (p["partition_id"], checked, p["partition_name"])
- )
-
- # recap des decisions jury (nombre dans chaque code):
- if codes_nb:
- H.append("
Décisions du jury
")
- cods = list(codes_nb.keys())
- cods.sort()
- for cod in cods:
- H.append("
tuple:
+ """Génère le tableau recap en excel (xlsx) ou CSV.
+ Utilisé pour archives et autres besoins particuliers (API).
+ Attention: le tableau exporté depuis la page html est celui généré en js par DataTables,
+ et non celui-ci.
+ """
+ suffix = scu.CSV_SUFFIX if format == "csv" else scu.XLSX_SUFFIX
+ filename += suffix
+ rows, footer_rows, titles, column_ids = res.get_table_recap(
+ convert_values=False, include_evaluations=include_evaluations
+ )
+
+ tab = GenTable(
+ columns_ids=column_ids,
+ titles=titles,
+ rows=rows + footer_rows,
+ preferences=sco_preferences.SemPreferences(formsemestre_id=formsemestre.id),
+ )
+
+ return tab.gen(format=format), filename
diff --git a/sco_version.py b/sco_version.py
index aced26cfe..4e9e9ad14 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.1.91"
+SCOVERSION = "9.1.92"
SCONAME = "ScoDoc"
From ec57ba4ef75cfac158b2942a5c81403988a8ee2e Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 7 Apr 2022 09:46:25 +0200
Subject: [PATCH 2/8] =?UTF-8?q?Mise=20=C3=A0=20jour=20bonus=20B=C3=A9thune?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/comp/bonus_spo.py | 43 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 36 insertions(+), 7 deletions(-)
diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py
index 5a549132b..416a0d0ee 100644
--- a/app/comp/bonus_spo.py
+++ b/app/comp/bonus_spo.py
@@ -418,17 +418,46 @@ class BonusAmiens(BonusSportAdditif):
class BonusBethune(BonusSportMultiplicatif):
- """Calcul bonus modules optionnels (sport), règle IUT de Béthune.
-
- 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.
+ """
+ Calcul bonus modules optionnels (sport, culture), règle IUT de Béthune.
+
+ Pour le BUT :
+ 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 %) = max(note-10, 0)*(1/500).
+
+ La bonification ne s'applique que si la note est supérieure à 10.
+
+ (Une note de 10 donne donc 0% de bonif,
+ 1 point au dessus de 10 augmente la moyenne des UE de 0.2%)
+
+
+ Pour le DUT/LP :
+ La note de sport est sur 20, et on calcule une bonification (en %)
+ qui va s'appliquer à la moyenne générale du semestre en appliquant
+ la formule : bonification (en %) = max(note-10, 0)*(1/200).
+
+ La bonification ne s'applique que si la note est supérieure à 10.
+
+ (Une note de 10 donne donc 0% de bonif,
+ 1 point au dessus de 10 augmente la moyenne des UE de 0.5%)
+
"""
name = "bonus_iutbethune"
displayed_name = "IUT de Béthune"
- seuil_moy_gen = 10.0
- amplitude = 0.005
+ 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"""
+ if self.formsemestre.formation.is_apc():
+ self.amplitude = 0.002
+ else:
+ self.amplitude = 0.005
+
+ return super().compute_bonus(
+ sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
+ )
class BonusBezier(BonusSportAdditif):
From 7a8c77add43ac1efe78fbe3ce53996720104c738 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Thu, 7 Apr 2022 23:31:08 +0200
Subject: [PATCH 3/8] =?UTF-8?q?Tableau=20saisie=20jury=20bas=C3=A9=20sur?=
=?UTF-8?q?=20recap=5Fcomplet?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/comp/res_common.py | 21 +-
app/scodoc/sco_formsemestre_status.py | 3 -
app/scodoc/sco_formsemestre_validation.py | 93 ++++++---
app/scodoc/sco_recapcomplet.py | 242 +++++++++++-----------
app/static/css/scodoc.css | 12 ++
app/static/js/table_recap.js | 86 ++++----
6 files changed, 260 insertions(+), 197 deletions(-)
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index d7240598a..7245439f4 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -18,7 +18,7 @@ from app.auth.models import User
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
-from app.models import FormSemestre, FormSemestreUECoef
+from app.models import FormSemestre, FormSemestreUECoef, formsemestre
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
@@ -388,7 +388,9 @@ class ResultatsSemestre(ResultatsCache):
# --- TABLEAU RECAP
- def get_table_recap(self, convert_values=False, include_evaluations=False):
+ def get_table_recap(
+ self, convert_values=False, include_evaluations=False, modejury=False
+ ):
"""Result: tuple avec
- rows: liste de dicts { column_id : value }
- titles: { column_id : title }
@@ -538,6 +540,9 @@ class ResultatsSemestre(ResultatsCache):
titles_bot[
f"_{col_id}_target_attrs"
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
+ if modejury:
+ # pas d'autre colonnes de résultats
+ continue
# Bonus (sport) dans cette UE ?
# Le bonus sport appliqué sur cette UE
if (self.bonus_ues is not None) and (ue.id in self.bonus_ues):
@@ -632,6 +637,18 @@ class ResultatsSemestre(ResultatsCache):
elif nb_ues_validables < len(ues_sans_bonus):
row["_ues_validables_class"] += " moy_inf"
row["_ues_validables_order"] = nb_ues_validables # pour tri
+ if modejury:
+ idx = add_cell(
+ row,
+ "jury_link",
+ "",
+ f"""saisir décision""",
+ "col_jury_link",
+ 1000,
+ )
rows.append(row)
self._recap_add_partitions(rows, titles)
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index 275605d88..50b11f6fa 100644
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -404,9 +404,6 @@ def formsemestre_status_menubar(sem):
"args": {
"formsemestre_id": formsemestre_id,
"modejury": 1,
- "hidemodules": 1,
- "hidebac": 1,
- "pref_override": 0,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
},
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index ba517e6ae..f4896a8dc 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -31,6 +31,7 @@ import time
import flask
from flask import url_for, g, request
+from app.models.etudiants import Identite
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@@ -107,29 +108,57 @@ def formsemestre_validation_etud_form(
if not Se.sem["etat"]:
raise ScoValueError("validation: semestre verrouille")
+ url_tableau = url_for(
+ "notes.formsemestre_recapcomplet",
+ scodoc_dept=g.scodoc_dept,
+ modejury=1,
+ formsemestre_id=formsemestre_id,
+ selected_etudid=etudid, # va a la bonne ligne
+ )
+
H = [
html_sco_header.sco_header(
- page_title="Parcours %(nomprenom)s" % etud,
+ page_title=f"Parcours {etud['nomprenom']}",
javascripts=["js/recap_parcours.js"],
)
]
- Footer = ["
utilise les coefficients d'UE pour calculer la moyenne générale.
+ """
+ )
+ H.append(html_sco_header.sco_footer())
# HTML or binary data ?
if len(H) > 1:
return "".join(H)
@@ -199,18 +185,15 @@ def formsemestre_recapcomplet(
return H
-def do_formsemestre_recapcomplet(
+def _do_formsemestre_recapcomplet(
formsemestre_id=None,
format="html", # html, xml, xls, xlsall, json
- hidemodules=False, # ne pas montrer les modules (ignoré en XML)
- hidebac=False, # pas de colonne Bac (ignoré en XML)
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
modejury=False, # saisie décisions jury
sortcol=None, # indice colonne a trier dans table T
xml_with_decisions=False,
- disable_etudlink=False,
- rank_partition_id=None, # si None, calcul rang global
force_publishing=True,
+ selected_etudid=None,
):
"""Calcule et renvoie le tableau récapitulatif."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
@@ -219,13 +202,15 @@ def do_formsemestre_recapcomplet(
f"""recap-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
- if (format == "html" or format == "evals") and not modejury:
+ if format == "html" or format == "evals":
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
data = gen_formsemestre_recapcomplet_html(
formsemestre,
res,
include_evaluations=(format == "evals"),
+ modejury=modejury,
filename=filename,
+ selected_etudid=selected_etudid,
)
return data
elif format.startswith("xls") or format == "csv":
@@ -388,35 +373,51 @@ def _gen_cell(key: str, row: dict, elt="td"):
return f"<{elt} {attrs}>{content}{elt}>"
-def _gen_row(keys: list[str], row, elt="td"):
+def _gen_row(keys: list[str], row, elt="td", selected_etudid=None):
klass = row.get("_tr_class")
tr_class = f'class="{klass}"' if klass else ""
- return f'
{"".join([_gen_cell(key, row, elt) for key in keys])}
'
+ tr_id = (
+ f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else ""
+ )
+ return f'
{"".join([_gen_cell(key, row, elt) for key in keys])}
'
def gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre,
res: NotesTableCompat,
include_evaluations=False,
+ modejury=False,
filename="",
+ selected_etudid=None,
):
"""Construit table recap pour le BUT
- Cache le résultat pour le semestre.
+ Cache le résultat pour le semestre (sauf en mode jury).
+
+ Si modejury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury
+
Return: data, filename
data est une chaine, le
...
incluant le tableau.
"""
- if include_evaluations:
- table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
- else:
- table_html = sco_cache.TableRecapCache.get(formsemestre.id)
- if table_html is None:
- table_html = _gen_formsemestre_recapcomplet_html(
- formsemestre, res, include_evaluations, filename
- )
+ table_html = None
+ if not (modejury or selected_etudid):
if include_evaluations:
- sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
+ table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
else:
- sco_cache.TableRecapCache.set(formsemestre.id, table_html)
+ table_html = sco_cache.TableRecapCache.get(formsemestre.id)
+ if modejury or (table_html is None):
+ table_html = _gen_formsemestre_recapcomplet_html(
+ formsemestre,
+ res,
+ include_evaluations,
+ modejury,
+ filename,
+ selected_etudid=selected_etudid,
+ )
+ if not modejury:
+ if include_evaluations:
+ sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
+ else:
+ sco_cache.TableRecapCache.set(formsemestre.id, table_html)
return table_html
@@ -425,11 +426,13 @@ def _gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre,
res: NotesTableCompat,
include_evaluations=False,
+ modejury=False,
filename: str = "",
+ selected_etudid=None,
) -> str:
"""Génère le html"""
rows, footer_rows, titles, column_ids = res.get_table_recap(
- convert_values=True, include_evaluations=include_evaluations
+ convert_values=True, include_evaluations=include_evaluations, modejury=modejury
)
if not rows:
return (
@@ -437,7 +440,8 @@ def _gen_formsemestre_recapcomplet_html(
)
H = [
f"""
"""
)
return "\n".join(H)
@@ -234,7 +240,7 @@ def _trombino_zip(groups_infos):
Z.writestr(filename, img)
Z.close()
size = data.tell()
- log("trombino_zip: %d bytes" % size)
+ log(f"trombino_zip: {size} bytes")
data.seek(0)
return send_file(
data,
@@ -470,7 +476,7 @@ def _listeappel_photos_pdf(groups_infos):
# --------------------- Upload des photos de tout un groupe
-def photos_generate_excel_sample(group_ids=[]):
+def photos_generate_excel_sample(group_ids=()):
"""Feuille excel pour import fichiers photos"""
fmt = sco_import_etuds.sco_import_format()
data = sco_import_etuds.sco_import_generate_excel_sample(
@@ -492,31 +498,33 @@ def photos_generate_excel_sample(group_ids=[]):
# return sco_excel.send_excel_file(data, "ImportPhotos" + scu.XLSX_SUFFIX)
-def photos_import_files_form(group_ids=[]):
+def photos_import_files_form(group_ids=()):
"""Formulaire pour importation photos"""
if not group_ids:
raise ScoValueError("paramètre manquant !")
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
- back_url = "groups_view?%s&curtab=tab-photos" % groups_infos.groups_query_args
+ back_url = f"groups_view?{groups_infos.groups_query_args}&curtab=tab-photos"
H = [
html_sco_header.sco_header(page_title="Import des photos des étudiants"),
- """
Téléchargement des photos des étudiants
-
Vous pouvez aussi charger les photos individuellement via la fiche de chaque étudiant (menu "Etudiant" / "Changer la photo").
-
Cette page permet de charger en une seule fois les photos de plusieurs étudiants.
- Il faut d'abord remplir une feuille excel donnant les noms
+ f"""
Téléchargement des photos des étudiants
+
Vous pouvez aussi charger les photos individuellement via la fiche
+ de chaque étudiant (menu "Etudiant" / "Changer la photo").
+
+
Cette page permet de charger en une seule fois les photos
+ de plusieurs étudiants.
+ Il faut d'abord remplir une feuille excel donnant les noms
des fichiers images (une image par étudiant).
-
Ensuite, réunir vos images dans un fichier zip, puis télécharger
+
Ensuite, réunir vos images dans un fichier zip, puis télécharger
simultanément le fichier excel et le fichier zip.
Absences
diff --git a/app/views/absences.py b/app/views/absences.py
index ae95d7eba..f664333af 100644
--- a/app/views/absences.py
+++ b/app/views/absences.py
@@ -69,7 +69,7 @@ from app.decorators import (
permission_required,
permission_required_compat_scodoc7,
)
-from app.models import FormSemestre
+from app.models import FormSemestre, GroupDescr
from app.models.absences import BilletAbsence
from app.views import absences_bp as bp
@@ -119,63 +119,71 @@ def sco_publish(route, function, permission, methods=["GET"]):
@scodoc7func
def index_html():
"""Gestionnaire absences, page principale"""
- # crude portage from 1999 DTML
- sems = sco_formsemestre.do_formsemestre_list()
- authuser = current_user
H = [
html_sco_header.sco_header(
- page_title="Gestion des absences",
+ page_title="Saisie des absences",
cssstyles=["css/calabs.css"],
javascripts=["js/calabs.js"],
),
- """
Gestion des Absences
""",
+ """
Traitement des absences
+
+ Pour saisir des absences ou consulter les états, il est recommandé par passer par
+ le semestre concerné (saisie par jours nommés ou par semaines).
+
+ """,
]
- if not sems:
+ H.append(
+ """
Pour signaler, annuler ou justifier une absence pour un seul étudiant,
+ choisissez d'abord concerné:
"""
+ )
+ H.append(sco_find_etud.form_search_etud())
+ if current_user.has_permission(
+ Permission.ScoAbsChange
+ ) and sco_preferences.get_preference("handle_billets_abs"):
H.append(
- """
Aucun semestre défini (ou aucun groupe d'étudiant)
- """
- % (gr_tit, sem["titre_num"], dayname, url_link_semaines, msg),
+ """,
]
#
if etuds:
@@ -820,7 +827,6 @@ def _gen_form_saisie_groupe(
# version pour formulaire avec AJAX (Yann LB)
H.append(
"""
-
@@ -831,8 +837,7 @@ def _gen_form_saisie_groupe(
Si vous "décochez" une case, l'absence correspondante sera supprimée.
Attention, les modifications sont automatiquement entregistrées au fur et à mesure.
- """
- % destination
+ """
)
return H
diff --git a/sco_version.py b/sco_version.py
index 4e9e9ad14..15814bf76 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.1.92"
+SCOVERSION = "9.2.0a"
SCONAME = "ScoDoc"
From 705aa54d775b395194f4f823f59ad854bc2b8502 Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sat, 9 Apr 2022 14:20:56 +0200
Subject: [PATCH 7/8] =?UTF-8?q?Table=20recap:=20liens=20vers=20=C3=A9valua?=
=?UTF-8?q?tions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/comp/res_common.py | 7 ++++++-
app/static/css/scodoc.css | 9 +++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index 7245439f4..60c7547db 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -680,7 +680,7 @@ class ResultatsSemestre(ResultatsCache):
row["_moy_gen_class"] = "col_moy_gen"
# titre de la ligne:
row["prenom"] = row["nom_short"] = (
- row.get(f"_title", "") or bottom_line.capitalize()
+ row.get("_title", "") or bottom_line.capitalize()
)
row["_tr_class"] = bottom_line.lower() + (
(" " + row["_tr_class"]) if "_tr_class" in row else ""
@@ -894,3 +894,8 @@ class ResultatsSemestre(ResultatsCache):
bottom_infos["min"][cid] = "0"
bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
bottom_infos["descr_evaluation"][cid] = e.description or ""
+ bottom_infos["descr_evaluation"][f"_{cid}_target"] = url_for(
+ "notes.evaluation_listenotes",
+ scodoc_dept=g.scodoc_dept,
+ evaluation_id=e.id,
+ )
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 3625d5f01..470b929a8 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -3730,6 +3730,15 @@ table.table_recap tr.descr_evaluation {
color: rgb(4, 16, 159);
}
+table.table_recap tr.descr_evaluation a {
+ color: rgb(4, 16, 159);
+ text-decoration: none;
+}
+
+table.table_recap tr.descr_evaluation a:hover {
+ color: red;
+}
+
table.table_recap tr.descr_evaluation {
vertical-align: top;
}
From 570e2dc308533aa819dd978e34ecc99ff448e37b Mon Sep 17 00:00:00 2001
From: Emmanuel Viennet
Date: Sun, 10 Apr 2022 17:38:59 +0200
Subject: [PATCH 8/8] =?UTF-8?q?Page=20=C3=A9tat=20de=20=C3=A9valuations=20?=
=?UTF-8?q?(closes=20#142).=20Am=C3=A9liore=20tableau=20recap.=20Cosm?=
=?UTF-8?q?=C3=A9tique.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/comp/moy_mod.py | 11 +-
app/comp/res_common.py | 27 +++--
app/models/moduleimpls.py | 1 -
app/scodoc/sco_evaluation_check_abs.py | 2 +-
app/scodoc/sco_evaluation_recap.py | 144 +++++++++++++++++++++++++
app/scodoc/sco_formsemestre_status.py | 5 +
app/scodoc/sco_liste_notes.py | 22 ++--
app/scodoc/sco_recapcomplet.py | 31 +-----
app/scodoc/sco_utils.py | 30 ++++++
app/static/css/scodoc.css | 89 ++++++++++++++-
app/static/js/evaluations_recap.js | 38 +++++++
app/views/notes.py | 11 +-
12 files changed, 359 insertions(+), 52 deletions(-)
create mode 100644 app/scodoc/sco_evaluation_recap.py
create mode 100644 app/static/js/evaluations_recap.js
diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py
index eea357e8f..13829a392 100644
--- a/app/comp/moy_mod.py
+++ b/app/comp/moy_mod.py
@@ -92,6 +92,8 @@ class ModuleImplResults:
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE.
"""
+ self.evals_etudids_sans_note = {}
+ """dict: evaluation_id : set des etudids non notés dans cette eval, sans les démissions."""
self.load_notes()
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
@@ -142,12 +144,13 @@ class ModuleImplResults:
# ou évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes
# car on calcule leur moyenne à part.
+ etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
- evaluation.publish_incomplete
- or (not (inscrits_module - set(eval_df.index)))
+ evaluation.publish_incomplete or (not etudids_sans_note)
)
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
+ self.evals_etudids_sans_note[evaluation.id] = etudids_sans_note
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
@@ -193,7 +196,9 @@ class ModuleImplResults:
return eval_df
def _etudids(self):
- """L'index du dataframe est la liste de tous les étudiants inscrits au semestre"""
+ """L'index du dataframe est la liste de tous les étudiants inscrits au semestre
+ (incluant les DEM et DEF)
+ """
return [
inscr.etudid
for inscr in ModuleImpl.query.get(
diff --git a/app/comp/res_common.py b/app/comp/res_common.py
index 60c7547db..7bc199ead 100644
--- a/app/comp/res_common.py
+++ b/app/comp/res_common.py
@@ -862,20 +862,27 @@ class ResultatsSemestre(ResultatsCache):
"_tr_class": "bottom_info",
"_title": "Description évaluation",
}
- first = True
+ first_eval = True
+ index_col = 9000 # à droite
for modimpl in self.formsemestre.modimpls_sorted:
evals = self.modimpls_results[modimpl.id].get_evaluations_completes(modimpl)
eval_index = len(evals) - 1
inscrits = {i.etudid for i in modimpl.inscriptions}
- klass = "evaluation first" if first else "evaluation"
- first = False
- for i, e in enumerate(evals):
+ first_eval_of_mod = True
+ for e in evals:
cid = f"eval_{e.id}"
titles[
cid
] = f'{modimpl.module.code} {eval_index} {e.jour.isoformat() if e.jour else ""}'
+ klass = "evaluation"
+ if first_eval:
+ klass += " first"
+ elif first_eval_of_mod:
+ klass += " first_of_mod"
titles[f"_{cid}_class"] = klass
- titles[f"_{cid}_col_order"] = 9000 + i # à droite
+ first_eval_of_mod = first_eval = False
+ titles[f"_{cid}_col_order"] = index_col
+ index_col += 1
eval_index -= 1
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
e.evaluation_id
@@ -889,7 +896,15 @@ class ResultatsSemestre(ResultatsCache):
# Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE
row[cid] = scu.fmt_note(val)
- row[f"_{cid}_class"] = klass
+ row[f"_{cid}_class"] = klass + {
+ "ABS": " abs",
+ "ATT": " att",
+ "EXC": " exc",
+ }.get(row[cid], "")
+ else:
+ row[cid] = "ni"
+ row[f"_{cid}_class"] = klass + " non_inscrit"
+
bottom_infos["coef"][cid] = e.coefficient
bottom_infos["min"][cid] = "0"
bottom_infos["max"][cid] = scu.fmt_note(e.note_max)
diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py
index 292ec8ffd..7574ed7e8 100644
--- a/app/models/moduleimpls.py
+++ b/app/models/moduleimpls.py
@@ -9,7 +9,6 @@ from app.comp import df_cache
from app.models.etudiants import Identite
from app.models.modules import Module
-import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu
diff --git a/app/scodoc/sco_evaluation_check_abs.py b/app/scodoc/sco_evaluation_check_abs.py
index b8287c25d..b464cd673 100644
--- a/app/scodoc/sco_evaluation_check_abs.py
+++ b/app/scodoc/sco_evaluation_check_abs.py
@@ -25,7 +25,7 @@
#
##############################################################################
-"""Vérification des abasneces à une évaluation
+"""Vérification des absences à une évaluation
"""
from flask import url_for, g
diff --git a/app/scodoc/sco_evaluation_recap.py b/app/scodoc/sco_evaluation_recap.py
new file mode 100644
index 000000000..168b463b8
--- /dev/null
+++ b/app/scodoc/sco_evaluation_recap.py
@@ -0,0 +1,144 @@
+##############################################################################
+# ScoDoc
+# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
+# See LICENSE
+##############################################################################
+
+"""Tableau recap. de toutes les évaluations d'un semestre
+avec leur état.
+
+Sur une idée de Pascal Bouron, de Lyon.
+"""
+import time
+from flask import g, url_for
+
+from app.models import Evaluation, FormSemestre
+from app.comp import res_sem
+from app.comp.res_compat import NotesTableCompat
+from app.comp.moy_mod import ModuleImplResults
+from app.scodoc import html_sco_header
+import app.scodoc.sco_utils as scu
+
+
+def evaluations_recap(formsemestre_id: int) -> str:
+ """Page récap. de toutes les évaluations d'un semestre"""
+ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ rows, titles = evaluations_recap_table(formsemestre)
+ column_ids = titles.keys()
+ filename = scu.sanitize_filename(
+ f"""evaluations-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
+ )
+ if not rows:
+ return '
aucune évaluation
'
+ H = [
+ html_sco_header.sco_header(
+ page_title="Évaluations du semestre",
+ javascripts=["js/evaluations_recap.js"],
+ ),
+ f"""