ScoDoc/app/scodoc/sco_recapcomplet.py

551 lines
20 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
2022-02-06 16:09:17 +01:00
"""Tableau récapitulatif des notes d'un semestre
2020-09-26 16:19:37 +02:00
"""
import collections
2021-02-04 20:02:44 +01:00
import datetime
import time
2021-07-10 17:40:40 +02:00
from xml.etree import ElementTree
2020-09-26 16:19:37 +02:00
from flask import abort, g, render_template, request, url_for
2021-08-29 19:57:32 +02:00
from app import log
2021-12-11 20:27:58 +01:00
from app.but import bulletin_but
from app.comp import res_sem
from app.comp.res_common import ResultatsSemestre
from app.comp.res_compat import NotesTableCompat
2021-12-17 21:59:28 +01:00
from app.models import FormSemestre
from app.models.etudiants import Identite
2021-12-11 20:27:58 +01:00
import app.scodoc.sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_xml
2022-04-05 22:23:55 +02:00
from app.scodoc import sco_cache
from app.scodoc import sco_evaluations
2022-04-06 18:51:01 +02:00
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
2023-02-03 22:39:45 +01:00
from app.tables.recap import TableRecap
2023-02-06 13:05:39 +01:00
from app.tables.jury_recap import TableJury
2020-09-26 16:19:37 +02:00
def formsemestre_recapcomplet(
formsemestre_id=None,
2022-06-29 16:30:01 +02:00
mode_jury=False,
2020-09-26 16:19:37 +02:00
tabformat="html",
xml_with_decisions=False,
force_publishing=True,
selected_etudid=None,
2020-09-26 16:19:37 +02:00
):
"""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.
2022-04-06 18:51:01 +02:00
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)
2022-06-29 16:30:01 +02:00
mode_jury: cache modules, affiche lien saisie decision jury
xml_with_decisions: publie décisions de jury dans xml et json
2024-03-01 12:03:19 +01:00
force_publishing: publie les xml et json même si bulletins non publiés (sur la passerelle)
selected_etudid: etudid sélectionné (pour scroller au bon endroit)
2020-09-26 16:19:37 +02:00
"""
if not isinstance(formsemestre_id, int):
abort(404)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
2022-05-09 18:32:54 +02:00
file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"}
supported_formats = file_formats | {"html", "evals"}
2022-05-09 18:32:54 +02:00
if tabformat not in supported_formats:
raise ScoValueError(f"Format non supporté: {tabformat}")
is_file = tabformat in file_formats
2022-06-29 16:30:01 +02:00
mode_jury = int(mode_jury)
2020-09-26 16:19:37 +02:00
xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing)
filename = scu.sanitize_filename(
2023-03-12 12:30:57 +01:00
f"""{'jury' if mode_jury else 'recap'
}{'-evals' if tabformat == 'xlsall' else ''
}-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}"""
)
if is_file:
return _formsemestre_recapcomplet_to_file(
formsemestre,
2023-03-12 12:30:57 +01:00
mode_jury=mode_jury,
tabformat=tabformat,
filename=filename,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
)
2022-05-09 18:32:54 +02:00
table_html, _, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
formsemestre,
filename=filename,
2022-06-29 16:30:01 +02:00
mode_jury=mode_jury,
tabformat=tabformat,
selected_etudid=selected_etudid,
2020-09-26 16:19:37 +02:00
)
H = [
# sco_formsemestre_status.formsemestre_status_head(
# formsemestre_id=formsemestre_id
# ),
]
if len(formsemestre.inscriptions) > 0:
H.append(
f"""<form name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
"""
)
2022-06-29 16:30:01 +02:00
if mode_jury:
2020-09-26 16:19:37 +02:00
H.append(
2022-06-29 16:30:01 +02:00
f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
2020-09-26 16:19:37 +02:00
)
H.append(
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
)
2023-06-18 09:37:13 +02:00
for fmt, label in (
("html", "Tableau"),
("evals", "Avec toutes les évaluations"),
2022-05-10 20:32:25 +02:00
("xlsx", "Excel (non formaté)"),
("xlsall", "Excel avec évaluations"),
("json", "Bulletins JSON"),
):
if fmt == tabformat:
selected = " selected"
else:
selected = ""
H.append(f'<option value="{fmt}"{selected}>{label}</option>')
H.append(
f"""
2023-12-06 20:04:40 +01:00
</select>&nbsp;<span class="help">cliquer sur un nom pour afficher son bulletin ou
<a class="stdlink"
href="{url_for('notes.formsemestre_bulletins_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
2023-12-06 20:04:40 +01:00
}">ici avoir le classeur pdf</a>
"""
)
if formsemestre.formation.is_apc():
H.append(
f"""&nbsp;ou en <a class="stdlink"
href="{url_for('notes.formsemestre_bulletins_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, version="butcourt")
}">version courte BUT</a>
"""
)
H.append(
"""</span>
</form>
"""
)
H.append(table_html) # La table
if len(formsemestre.inscriptions) > 0:
H.append("""<div class="links_under_recap"><ul>""")
2023-02-18 18:49:52 +01:00
if not mode_jury:
H.append(
f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Décisions du jury</a>
</li>
"""
)
if formsemestre.can_edit_jury():
2022-06-29 16:30:01 +02:00
if mode_jury:
2022-03-27 23:19:17 +02:00
H.append(
f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Calcul automatique des décisions du jury</a>
</li>
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_erase',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
}">Effacer <em>toutes</em> les décisions de jury issues de ce semestre</a>
</li>
"""
)
2023-02-18 18:49:52 +01:00
if mode_jury:
H.append(
f"""<li><a class="stdlink" href="{
url_for('notes.formsemestre_lettres_individuelles',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">Courriers individuels (classeur pdf)</a>
</li>
"""
)
H.append(
f"""<li><a class="stdlink" href="{url_for('notes.formsemestre_pvjury_pdf',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
}">PV officiel (pdf)</a>
</li>
"""
)
H.append("</ul></div>")
if sco_preferences.get_preference("use_ue_coefs", formsemestre_id):
H.append(
"""
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>
"""
)
if mode_jury and freq_codes_annuels and sum(freq_codes_annuels.values()) > 0:
nb_etud_avec_decision_annuelle = (
sum(freq_codes_annuels.values()) - freq_codes_annuels["total"]
)
H.append(
f"""
<div class="jury_stats">
<div><b>Nb d'étudiants avec décision annuelle:</b>
{nb_etud_avec_decision_annuelle} / {freq_codes_annuels["total"]}
</div>
"""
)
if nb_etud_avec_decision_annuelle > 0:
H.append(
"""<div><b>Codes annuels octroyés:</b></div>
<table class="jury_stats_codes">
"""
)
for code in sorted(freq_codes_annuels.keys()):
if code != "total":
H.append(
f"""<tr>
<td>{code}</td>
<td style="text-align:right">{freq_codes_annuels[code]}</td>
<td style="text-align:right">{
(100*freq_codes_annuels[code] / freq_codes_annuels["total"]):2.1f}%
</td>
</tr>"""
)
H.append("""</table>""")
H.append("""</div>""")
# Légende
H.append(
"""
<div class="table_recap_caption">
<div class="title">Codes utilisés dans cette table:</div>
<div class="captions">
<div><tt>~</tt></div><div>valeur manquante</div>
<div><tt>=</tt></div><div>UE dispensée</div>
<div><tt>nan</tt></div><div>valeur non disponible</div>
<div>📍</div><div>code jury non enregistré</div>
<div><span class="ue_hors_parcours">12.34</span></div><div>UE hors parcours</div>
</div>
</div>
"""
)
# HTML or binary data ?
if len(H) > 1:
return render_template(
"sco_page.j2",
content="".join(H),
title=f"{formsemestre.sem_modalite()}: "
+ ("jury" if mode_jury else "moyennes"),
javascripts=["js/table_recap.js"],
formsemestre_id=formsemestre_id,
no_sidebar=True,
)
elif len(H) == 1:
return H[0]
else:
return H
2020-09-26 16:19:37 +02:00
def _formsemestre_recapcomplet_to_html(
formsemestre: FormSemestre,
tabformat="html", # "html" or "evals"
filename: str = "",
2022-06-29 16:30:01 +02:00
mode_jury=False, # saisie décisions jury
selected_etudid=None,
) -> tuple[str, TableRecap, collections.Counter]:
"""Le tableau recap en html"""
if tabformat not in ("html", "evals"):
raise ScoValueError("invalid table format")
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
formsemestre,
res,
include_evaluations=(tabformat == "evals"),
mode_jury=mode_jury,
filename=filename,
selected_etudid=selected_etudid,
)
return table_html, table, freq_codes_annuels
def _formsemestre_recapcomplet_to_file(
formsemestre: FormSemestre,
tabformat: str = "json", # xml, xls, xlsall, json
2023-03-12 12:30:57 +01:00
mode_jury: bool = False,
filename: str = "",
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
2020-09-26 16:19:37 +02:00
xml_with_decisions=False,
force_publishing=True,
2020-09-26 16:19:37 +02:00
):
"""Calcule et renvoie le tableau récapitulatif."""
if tabformat.startswith("xls"):
2023-03-12 12:30:57 +01:00
include_evaluations = tabformat == "xlsall"
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
2022-04-06 18:51:01 +02:00
data, filename = gen_formsemestre_recapcomplet_excel(
res,
2023-03-12 12:30:57 +01:00
mode_jury=mode_jury,
2022-04-06 18:51:01 +02:00
include_evaluations=include_evaluations,
filename=filename,
)
2023-03-12 12:30:57 +01:00
mime, suffix = scu.get_mime_suffix("xlsx")
return scu.send_file(data, filename=filename, mime=mime, suffix=suffix)
elif tabformat == "xml":
2022-04-06 18:51:01 +02:00
data = gen_formsemestre_recapcomplet_xml(
formsemestre.id,
xml_nodate,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
2020-09-26 16:19:37 +02:00
)
2022-04-06 18:51:01 +02:00
return scu.send_file(data, filename=filename, suffix=scu.XML_SUFFIX)
elif tabformat == "json":
2022-04-06 18:51:01 +02:00
data = gen_formsemestre_recapcomplet_json(
formsemestre.id,
xml_nodate=xml_nodate,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
)
2022-04-06 18:51:01 +02:00
return scu.sendJSON(data, filename=filename)
raise ScoValueError(f"Format demandé invalide: {tabformat}")
2020-09-26 16:19:37 +02:00
2022-04-06 18:51:01 +02:00
def gen_formsemestre_recapcomplet_xml(
formsemestre_id,
xml_nodate,
xml_with_decisions=False,
force_publishing=True,
2022-04-06 18:51:01 +02:00
) -> str:
"XML export: liste tous les bulletins XML."
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
2022-02-12 22:57:46 +01:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
2020-09-26 16:19:37 +02:00
T = nt.get_table_moyennes_triees()
if not T:
return "", "", "xml"
if xml_nodate:
docdate = ""
else:
docdate = datetime.datetime.now().isoformat()
2021-07-10 17:40:40 +02:00
doc = ElementTree.Element(
2021-08-22 17:18:15 +02:00
"recapsemestre", formsemestre_id=str(formsemestre_id), date=docdate
2021-07-10 17:40:40 +02:00
)
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
2021-07-10 17:40:40 +02:00
doc.append(
ElementTree.Element(
"evals_info",
nb_evals_completes=str(evals["nb_evals_completes"]),
nb_evals_en_cours=str(evals["nb_evals_en_cours"]),
nb_evals_vides=str(evals["nb_evals_vides"]),
date_derniere_note=str(evals["last_modif"]),
)
2020-09-26 16:19:37 +02:00
)
for t in T:
etudid = t[-1]
sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre_id,
etudid,
doc=doc,
force_publishing=force_publishing,
2020-09-26 16:19:37 +02:00
xml_nodate=xml_nodate,
xml_with_decisions=xml_with_decisions,
)
2022-04-06 18:51:01 +02:00
return ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
2022-04-06 18:51:01 +02:00
def gen_formsemestre_recapcomplet_json(
formsemestre_id,
xml_nodate=False,
xml_with_decisions=False,
force_publishing=True,
2022-04-06 18:51:01 +02:00
) -> dict:
"""JSON export: liste tous les bulletins JSON
:param xml_nodate(bool): indique la date courante (attribut docdate)
2024-03-01 12:03:19 +01:00
:param force_publishing: donne les bulletins même si non "publiés sur la passerelle"
2022-04-06 18:51:01 +02:00
:returns: dict
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
2021-12-17 21:59:28 +01:00
is_apc = formsemestre.formation.is_apc()
if xml_nodate:
docdate = ""
else:
docdate = datetime.datetime.now().isoformat()
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
2022-04-06 18:51:01 +02:00
js_data = {
"docdate": docdate,
"formsemestre_id": formsemestre_id,
"evals_info": {
"nb_evals_completes": evals["nb_evals_completes"],
"nb_evals_en_cours": evals["nb_evals_en_cours"],
"nb_evals_vides": evals["nb_evals_vides"],
"date_derniere_note": evals["last_modif"],
},
"bulletins": [],
}
2022-04-06 18:51:01 +02:00
bulletins = js_data["bulletins"]
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
2022-02-12 22:57:46 +01:00
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
T = nt.get_table_moyennes_triees()
for t in T:
etudid = t[-1]
2021-12-17 21:59:28 +01:00
if is_apc:
etud = Identite.get_etud(etudid)
bulletins_sem = bulletin_but.BulletinBUT(formsemestre)
2023-09-01 14:38:41 +02:00
bul = bulletins_sem.bulletin_etud(etud)
2021-12-17 21:59:28 +01:00
else:
bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
formsemestre_id,
etudid,
force_publishing=force_publishing,
xml_with_decisions=xml_with_decisions,
)
2021-12-17 21:59:28 +01:00
bulletins.append(bul)
2022-04-06 18:51:01 +02:00
return js_data
def formsemestres_bulletins(annee_scolaire):
2024-03-01 12:03:19 +01:00
"""Tous les bulletins des semestres de l'année indiquée.
2022-02-09 23:22:00 +01:00
:param annee_scolaire(int): année de début de l'année scolaire
:returns: JSON
"""
2022-04-06 18:51:01 +02:00
js_list = []
2021-08-19 10:28:35 +02:00
sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire)
2024-03-01 12:03:19 +01:00
log(f"formsemestres_bulletins({annee_scolaire}): {len(sems)} sems")
for sem in sems:
2022-04-06 18:51:01 +02:00
js_data = gen_formsemestre_recapcomplet_json(
sem["formsemestre_id"], force_publishing=False
)
2022-04-06 18:51:01 +02:00
js_list.append(js_data)
2022-04-06 18:51:01 +02:00
return scu.sendJSON(js_list)
2022-03-26 23:33:57 +01:00
def gen_formsemestre_recapcomplet_html_table(
2022-04-06 18:51:01 +02:00
formsemestre: FormSemestre,
res: NotesTableCompat,
include_evaluations=False,
2022-06-29 16:30:01 +02:00
mode_jury=False,
2022-04-06 18:51:01 +02:00
filename="",
selected_etudid=None,
) -> tuple[str, TableRecap, collections.Counter]:
2022-03-26 23:33:57 +01:00
"""Construit table recap pour le BUT
Cache le résultat pour le semestre.
Note: on cache le HTML et non l'objet Table.
Si mode_jury, occultera colonnes modules (en js)
et affiche un lien vers la saisie de la décision de jury
Return: html (str), table (None sauf en mode jury ou si pas cachée)
html est une chaine, le <div>...</div> incluant le tableau.
2022-03-26 23:33:57 +01:00
"""
table = None
table_html = None
table_html_cached = None
cache_class = {
(True, True): sco_cache.TableJuryWithEvalsCache,
(True, False): sco_cache.TableJuryCache,
(False, True): sco_cache.TableRecapWithEvalsCache,
(False, False): sco_cache.TableRecapCache,
}[(bool(mode_jury), bool(include_evaluations))]
2023-02-21 00:45:27 +01:00
if not selected_etudid:
table_html_cached = cache_class.get(formsemestre.id)
if table_html_cached is None:
table = _gen_formsemestre_recapcomplet_table(
res,
include_evaluations,
2022-06-29 16:30:01 +02:00
mode_jury,
filename,
selected_etudid=selected_etudid,
)
table_html = table.html()
freq_codes_annuels = (
table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
)
cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
else:
table_html, freq_codes_annuels = table_html_cached
2022-04-05 22:23:55 +02:00
return table_html, table, freq_codes_annuels
2022-04-05 22:23:55 +02:00
def _gen_formsemestre_recapcomplet_table(
res: ResultatsSemestre,
2022-04-05 22:23:55 +02:00
include_evaluations=False,
2022-06-29 16:30:01 +02:00
mode_jury=False,
2023-03-12 12:30:57 +01:00
convert_values: bool = True,
2022-04-05 22:23:55 +02:00
filename: str = "",
selected_etudid=None,
) -> TableRecap:
"""Construit la table récap."""
table_class = TableJury if mode_jury else TableRecap
table = table_class(
2023-02-03 22:39:45 +01:00
res,
2023-03-12 12:30:57 +01:00
convert_values=convert_values,
2022-06-29 16:30:01 +02:00
include_evaluations=include_evaluations,
mode_jury=mode_jury,
2023-03-12 12:30:57 +01:00
read_only=not res.formsemestre.can_edit_jury(),
)
2023-02-03 22:39:45 +01:00
2023-01-29 21:52:39 +01:00
table.data["filename"] = filename
table.select_row(selected_etudid)
return table
2022-04-06 18:51:01 +02:00
def gen_formsemestre_recapcomplet_excel(
res: NotesTableCompat,
2023-03-12 12:30:57 +01:00
mode_jury: bool = False,
2022-04-06 18:51:01 +02:00
include_evaluations=False,
filename: str = "",
) -> tuple:
2023-03-12 12:30:57 +01:00
"""Génère le tableau recap ou jury en excel (xlsx).
Utilisé pour menu (export excel), archives et autres besoins particuliers (API).
2022-04-06 18:51:01 +02:00
Attention: le tableau exporté depuis la page html est celui généré en js par DataTables,
et non celui-ci.
"""
2023-03-12 12:30:57 +01:00
table = _gen_formsemestre_recapcomplet_table(
2023-02-03 22:39:45 +01:00
res,
include_evaluations=include_evaluations,
2023-03-12 12:30:57 +01:00
mode_jury=mode_jury,
convert_values=False,
filename=filename,
2022-04-06 18:51:01 +02:00
)
return table.excel(), filename