553 lines
20 KiB
Python
553 lines
20 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
#
|
|
##############################################################################
|
|
|
|
"""Tableau récapitulatif des notes d'un semestre
|
|
"""
|
|
import collections
|
|
import datetime
|
|
import time
|
|
from xml.etree import ElementTree
|
|
|
|
from flask import g, request
|
|
from flask import abort, url_for
|
|
|
|
from app import log
|
|
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
|
|
from app.models import FormSemestre
|
|
from app.models.etudiants import Identite
|
|
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_bulletins_json
|
|
from app.scodoc import sco_bulletins_xml
|
|
from app.scodoc import sco_cache
|
|
from app.scodoc import sco_evaluations
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_formsemestre_status
|
|
from app.scodoc import sco_preferences
|
|
from app.tables.recap import TableRecap
|
|
from app.tables.jury_recap import TableJury
|
|
|
|
|
|
def formsemestre_recapcomplet(
|
|
formsemestre_id=None,
|
|
mode_jury=False,
|
|
tabformat="html",
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
selected_etudid=None,
|
|
):
|
|
"""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)
|
|
|
|
mode_jury: cache modules, affiche lien saisie decision jury
|
|
xml_with_decisions: publie décisions de jury dans xml et json
|
|
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)
|
|
"""
|
|
if not isinstance(formsemestre_id, int):
|
|
abort(404)
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
file_formats = {"csv", "json", "xls", "xlsx", "xlsall", "xml"}
|
|
supported_formats = file_formats | {"html", "evals"}
|
|
if tabformat not in supported_formats:
|
|
raise ScoValueError(f"Format non supporté: {tabformat}")
|
|
is_file = tabformat in file_formats
|
|
mode_jury = int(mode_jury)
|
|
xml_with_decisions = int(xml_with_decisions)
|
|
force_publishing = int(force_publishing)
|
|
filename = scu.sanitize_filename(
|
|
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,
|
|
mode_jury=mode_jury,
|
|
tabformat=tabformat,
|
|
filename=filename,
|
|
xml_with_decisions=xml_with_decisions,
|
|
force_publishing=force_publishing,
|
|
)
|
|
|
|
table_html, table, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
|
|
formsemestre,
|
|
filename=filename,
|
|
mode_jury=mode_jury,
|
|
tabformat=tabformat,
|
|
selected_etudid=selected_etudid,
|
|
)
|
|
|
|
H = [
|
|
html_sco_header.sco_header(
|
|
page_title=f"{formsemestre.sem_modalite()}: "
|
|
+ ("jury" if mode_jury else "moyennes"),
|
|
no_side_bar=True,
|
|
init_qtip=True,
|
|
javascripts=["js/etud_info.js", "js/table_recap.js"],
|
|
),
|
|
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>
|
|
"""
|
|
)
|
|
if mode_jury:
|
|
H.append(
|
|
f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
|
|
)
|
|
H.append(
|
|
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
|
)
|
|
for fmt, label in (
|
|
("html", "Tableau"),
|
|
("evals", "Avec toutes les évaluations"),
|
|
("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"""
|
|
</select> <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)
|
|
}">ici avoir le classeur pdf</a>
|
|
"""
|
|
)
|
|
if formsemestre.formation.is_apc():
|
|
H.append(
|
|
f""" 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>""")
|
|
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():
|
|
if mode_jury:
|
|
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_but_erase',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
|
|
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a>
|
|
</li>
|
|
"""
|
|
)
|
|
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>
|
|
</div>
|
|
"""
|
|
)
|
|
H.append(html_sco_header.sco_footer())
|
|
# HTML or binary data ?
|
|
if len(H) > 1:
|
|
return "".join(H)
|
|
elif len(H) == 1:
|
|
return H[0]
|
|
else:
|
|
return H
|
|
|
|
|
|
def _formsemestre_recapcomplet_to_html(
|
|
formsemestre: FormSemestre,
|
|
tabformat="html", # "html" or "evals"
|
|
filename: str = "",
|
|
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
|
|
mode_jury: bool = False,
|
|
filename: str = "",
|
|
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
):
|
|
"""Calcule et renvoie le tableau récapitulatif."""
|
|
if tabformat.startswith("xls"):
|
|
include_evaluations = tabformat == "xlsall"
|
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
data, filename = gen_formsemestre_recapcomplet_excel(
|
|
res,
|
|
mode_jury=mode_jury,
|
|
include_evaluations=include_evaluations,
|
|
filename=filename,
|
|
)
|
|
mime, suffix = scu.get_mime_suffix("xlsx")
|
|
return scu.send_file(data, filename=filename, mime=mime, suffix=suffix)
|
|
elif tabformat == "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 tabformat == "json":
|
|
data = gen_formsemestre_recapcomplet_json(
|
|
formsemestre.id,
|
|
xml_nodate=xml_nodate,
|
|
xml_with_decisions=xml_with_decisions,
|
|
force_publishing=force_publishing,
|
|
)
|
|
return scu.sendJSON(data, filename=filename)
|
|
|
|
raise ScoValueError(f"Format demandé invalide: {tabformat}")
|
|
|
|
|
|
def gen_formsemestre_recapcomplet_xml(
|
|
formsemestre_id,
|
|
xml_nodate,
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
) -> str:
|
|
"XML export: liste tous les bulletins XML."
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
T = nt.get_table_moyennes_triees()
|
|
if not T:
|
|
return "", "", "xml"
|
|
|
|
if xml_nodate:
|
|
docdate = ""
|
|
else:
|
|
docdate = datetime.datetime.now().isoformat()
|
|
doc = ElementTree.Element(
|
|
"recapsemestre", formsemestre_id=str(formsemestre_id), date=docdate
|
|
)
|
|
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre)
|
|
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"]),
|
|
)
|
|
)
|
|
for t in T:
|
|
etudid = t[-1]
|
|
sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
|
|
formsemestre_id,
|
|
etudid,
|
|
doc=doc,
|
|
force_publishing=force_publishing,
|
|
xml_nodate=xml_nodate,
|
|
xml_with_decisions=xml_with_decisions,
|
|
)
|
|
return ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
|
|
|
|
|
def gen_formsemestre_recapcomplet_json(
|
|
formsemestre_id,
|
|
xml_nodate=False,
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
) -> dict:
|
|
"""JSON export: liste tous les bulletins JSON
|
|
:param xml_nodate(bool): indique la date courante (attribut docdate)
|
|
:param force_publishing: donne les bulletins même si non "publiés sur la passerelle"
|
|
:returns: dict
|
|
"""
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
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)
|
|
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": [],
|
|
}
|
|
bulletins = js_data["bulletins"]
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
T = nt.get_table_moyennes_triees()
|
|
for t in T:
|
|
etudid = t[-1]
|
|
if is_apc:
|
|
etud = Identite.get_etud(etudid)
|
|
bulletins_sem = bulletin_but.BulletinBUT(formsemestre)
|
|
bul = bulletins_sem.bulletin_etud(etud)
|
|
else:
|
|
bul = sco_bulletins_json.formsemestre_bulletinetud_published_dict(
|
|
formsemestre_id,
|
|
etudid,
|
|
force_publishing=force_publishing,
|
|
xml_with_decisions=xml_with_decisions,
|
|
)
|
|
bulletins.append(bul)
|
|
return js_data
|
|
|
|
|
|
def formsemestres_bulletins(annee_scolaire):
|
|
"""Tous les bulletins des semestres de l'année indiquée.
|
|
:param annee_scolaire(int): année de début de l'année scolaire
|
|
:returns: JSON
|
|
"""
|
|
js_list = []
|
|
sems = sco_formsemestre.list_formsemestre_by_etape(annee_scolaire=annee_scolaire)
|
|
log(f"formsemestres_bulletins({annee_scolaire}): {len(sems)} sems")
|
|
for sem in sems:
|
|
js_data = gen_formsemestre_recapcomplet_json(
|
|
sem["formsemestre_id"], force_publishing=False
|
|
)
|
|
js_list.append(js_data)
|
|
|
|
return scu.sendJSON(js_list)
|
|
|
|
|
|
def gen_formsemestre_recapcomplet_html_table(
|
|
formsemestre: FormSemestre,
|
|
res: NotesTableCompat,
|
|
include_evaluations=False,
|
|
mode_jury=False,
|
|
filename="",
|
|
selected_etudid=None,
|
|
) -> tuple[str, TableRecap, collections.Counter]:
|
|
"""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.
|
|
"""
|
|
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))]
|
|
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,
|
|
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
|
|
|
|
return table_html, table, freq_codes_annuels
|
|
|
|
|
|
def _gen_formsemestre_recapcomplet_table(
|
|
res: ResultatsSemestre,
|
|
include_evaluations=False,
|
|
mode_jury=False,
|
|
convert_values: bool = True,
|
|
filename: str = "",
|
|
selected_etudid=None,
|
|
) -> TableRecap:
|
|
"""Construit la table récap."""
|
|
table_class = TableJury if mode_jury else TableRecap
|
|
table = table_class(
|
|
res,
|
|
convert_values=convert_values,
|
|
include_evaluations=include_evaluations,
|
|
mode_jury=mode_jury,
|
|
read_only=not res.formsemestre.can_edit_jury(),
|
|
)
|
|
|
|
table.data["filename"] = filename
|
|
table.select_row(selected_etudid)
|
|
return table
|
|
|
|
|
|
def gen_formsemestre_recapcomplet_excel(
|
|
res: NotesTableCompat,
|
|
mode_jury: bool = False,
|
|
include_evaluations=False,
|
|
filename: str = "",
|
|
) -> tuple:
|
|
"""Génère le tableau recap ou jury en excel (xlsx).
|
|
Utilisé pour menu (export excel), 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.
|
|
"""
|
|
table = _gen_formsemestre_recapcomplet_table(
|
|
res,
|
|
include_evaluations=include_evaluations,
|
|
mode_jury=mode_jury,
|
|
convert_values=False,
|
|
filename=filename,
|
|
)
|
|
|
|
return table.excel(), filename
|