forked from ScoDoc/DocScoDoc
486 lines
17 KiB
Python
486 lines
17 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2023 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 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_compat import NotesTableCompat
|
|
from app.models import FormSemestre
|
|
from app.models.etudiants import Identite
|
|
|
|
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
|
|
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_permissions_check
|
|
from app.scodoc import sco_preferences
|
|
|
|
|
|
def formsemestre_recapcomplet(
|
|
formsemestre_id=None,
|
|
mode_jury=False,
|
|
tabformat="html",
|
|
sortcol=None,
|
|
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
|
|
selected_etudid: etudid sélectionné (pour scroller au bon endroit)
|
|
"""
|
|
if not isinstance(formsemestre_id, int):
|
|
abort(404)
|
|
formsemestre = FormSemestre.query.get_or_404(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)
|
|
|
|
data = _do_formsemestre_recapcomplet(
|
|
formsemestre_id,
|
|
format=tabformat,
|
|
mode_jury=mode_jury,
|
|
sortcol=sortcol,
|
|
xml_with_decisions=xml_with_decisions,
|
|
force_publishing=force_publishing,
|
|
selected_etudid=selected_etudid,
|
|
)
|
|
if is_file:
|
|
return data
|
|
H = [
|
|
html_sco_header.sco_header(
|
|
page_title=f"{formsemestre.sem_modalite()}: 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 (format, label) in (
|
|
("html", "Tableau"),
|
|
("evals", "Avec toutes les évaluations"),
|
|
("xlsx", "Excel (non formaté)"),
|
|
("xlsall", "Excel avec évaluations"),
|
|
("xml", "Bulletins XML (obsolète)"),
|
|
("json", "Bulletins JSON"),
|
|
):
|
|
if format == tabformat:
|
|
selected = " selected"
|
|
else:
|
|
selected = ""
|
|
H.append(f'<option value="{format}"{selected}>{label}</option>')
|
|
H.append("</select>")
|
|
|
|
H.append(
|
|
f""" (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 papier</a>)
|
|
"""
|
|
)
|
|
H.append(data)
|
|
|
|
if len(formsemestre.inscriptions) > 0:
|
|
H.append("</form>")
|
|
H.append(
|
|
f"""<p><a class="stdlink" href="{url_for('notes.formsemestre_pvjury',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
|
}">Voir les décisions du jury</a></p>"""
|
|
)
|
|
if sco_permissions_check.can_validate_sem(formsemestre_id):
|
|
H.append("<p>")
|
|
if mode_jury:
|
|
H.append(
|
|
f"""<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></p>"""
|
|
)
|
|
else:
|
|
H.append(
|
|
f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
|
|
}">Saisie des décisions du jury</a>"""
|
|
)
|
|
H.append("</p>")
|
|
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>
|
|
"""
|
|
)
|
|
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 _do_formsemestre_recapcomplet(
|
|
formsemestre_id=None,
|
|
format="html", # html, xml, xls, xlsall, json
|
|
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
|
|
mode_jury=False, # saisie décisions jury
|
|
sortcol=None, # indice colonne a trier dans table T
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
selected_etudid=None,
|
|
):
|
|
"""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":
|
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
data = gen_formsemestre_recapcomplet_html(
|
|
formsemestre,
|
|
res,
|
|
include_evaluations=(format == "evals"),
|
|
mode_jury=mode_jury,
|
|
filename=filename,
|
|
selected_etudid=selected_etudid,
|
|
)
|
|
return data
|
|
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,
|
|
)
|
|
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":
|
|
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: {format}")
|
|
|
|
|
|
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.query.get_or_404(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_id)
|
|
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 portail"
|
|
:returns: dict
|
|
"""
|
|
formsemestre = FormSemestre.query.get_or_404(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_id)
|
|
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.query.get_or_404(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.query.get(etudid)
|
|
r = bulletin_but.BulletinBUT(formsemestre)
|
|
bul = r.bulletin_etud(etud, formsemestre)
|
|
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 publiés 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("formsemestres_bulletins(%s): %d sems" % (annee_scolaire, len(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(
|
|
formsemestre: FormSemestre,
|
|
res: NotesTableCompat,
|
|
include_evaluations=False,
|
|
mode_jury=False,
|
|
filename="",
|
|
selected_etudid=None,
|
|
):
|
|
"""Construit table recap pour le BUT
|
|
Cache le résultat pour le semestre (sauf en mode jury).
|
|
|
|
Si mode_jury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury
|
|
|
|
Return: data, filename
|
|
data est une chaine, le <div>...</div> incluant le tableau.
|
|
"""
|
|
table_html = None
|
|
if not (mode_jury or selected_etudid):
|
|
if include_evaluations:
|
|
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
|
|
else:
|
|
table_html = sco_cache.TableRecapCache.get(formsemestre.id)
|
|
# en mode jury ne cache pas la table html
|
|
if mode_jury or (table_html is None):
|
|
table_html = _gen_formsemestre_recapcomplet_html(
|
|
formsemestre,
|
|
res,
|
|
include_evaluations,
|
|
mode_jury,
|
|
filename,
|
|
selected_etudid=selected_etudid,
|
|
)
|
|
if not mode_jury:
|
|
if include_evaluations:
|
|
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
|
|
else:
|
|
sco_cache.TableRecapCache.set(formsemestre.id, table_html)
|
|
|
|
return table_html
|
|
|
|
|
|
def _gen_formsemestre_recapcomplet_html(
|
|
formsemestre: FormSemestre,
|
|
res: NotesTableCompat,
|
|
include_evaluations=False,
|
|
mode_jury=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,
|
|
mode_jury=mode_jury,
|
|
)
|
|
if not rows:
|
|
return (
|
|
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
|
|
)
|
|
H = [
|
|
f"""<div class="table_recap"><table class="table_recap {
|
|
'apc' if formsemestre.formation.is_apc() else 'classic'
|
|
} {'jury' if mode_jury else ''}"
|
|
data-filename="{filename}">"""
|
|
]
|
|
# header
|
|
H.append(
|
|
f"""
|
|
<thead>
|
|
{scu.gen_row(column_ids, titles, "th")}
|
|
</thead>
|
|
"""
|
|
)
|
|
# body
|
|
H.append("<tbody>")
|
|
for row in rows:
|
|
H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n")
|
|
H.append("</tbody>\n")
|
|
# footer
|
|
H.append("<tfoot>")
|
|
idx_last = len(footer_rows) - 1
|
|
for i, row in enumerate(footer_rows):
|
|
H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
|
|
H.append(
|
|
"""
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
"""
|
|
)
|
|
return "".join(H)
|
|
|
|
|
|
def gen_formsemestre_recapcomplet_excel(
|
|
formsemestre: FormSemestre,
|
|
res: NotesTableCompat,
|
|
include_evaluations=False,
|
|
filename: str = "",
|
|
format="xls",
|
|
) -> 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
|