ScoDocMM/app/scodoc/sco_recapcomplet.py

497 lines
17 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2022-01-01 14:49:42 +01:00
# Copyright (c) 1999 - 2022 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
"""
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 g, request
from flask import make_response, 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_compat import NotesTableCompat
2021-12-17 21:59:28 +01:00
from app.models import FormSemestre
from app.models.etudiants import Identite
2022-02-06 16:09:17 +01:00
from app.models.evaluations import Evaluation
2021-12-11 20:27:58 +01:00
2022-04-06 18:51:01 +02:00
from app.scodoc.gen_tables import GenTable
2021-12-11 20:27:58 +01:00
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_bulletins, sco_excel
2022-04-05 22:23:55 +02:00
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
2022-04-06 18:51:01 +02:00
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
from app.scodoc import sco_groups
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
from app.scodoc import sco_users
2021-07-10 17:40:40 +02:00
from app.scodoc import sco_xml
from app.scodoc.sco_codes_parcours import DEF, UE_SPORT
2020-09-26 16:19:37 +02:00
def formsemestre_recapcomplet(
formsemestre_id=None,
modejury=False, # affiche lien saisie decision jury
tabformat="html",
sortcol=None,
xml_with_decisions=False, # XML avec decisions
rank_partition_id=None, # si None, calcul rang global
force_publishing=True, # publie les XML/JSON meme si bulletins non publiés
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)
modejury: cache modules, affiche lien saisie decision jury
2020-09-26 16:19:37 +02:00
"""
2021-12-11 20:27:58 +01:00
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
2022-04-06 18:51:01 +02:00
2020-09-26 16:19:37 +02:00
modejury = int(modejury)
2022-04-06 18:51:01 +02:00
2020-09-26 16:19:37 +02:00
xml_with_decisions = int(xml_with_decisions)
force_publishing = int(force_publishing)
2022-04-06 18:51:01 +02:00
is_file = tabformat in {"csv", "json", "xls", "xlsx", "xlsall", "xml"}
2020-09-26 16:19:37 +02:00
H = []
2022-04-06 18:51:01 +02:00
if not is_file:
2020-09-26 16:19:37 +02:00
H += [
2021-06-13 23:37:14 +02:00
html_sco_header.sco_header(
2020-09-26 16:19:37 +02:00
page_title="Récapitulatif",
no_side_bar=True,
init_qtip=True,
2022-03-26 23:33:57 +01:00
javascripts=["js/etud_info.js", "js/table_recap.js"],
2020-09-26 16:19:37 +02:00
),
sco_formsemestre_status.formsemestre_status_head(
formsemestre_id=formsemestre_id
2020-09-26 16:19:37 +02:00
),
]
2022-03-27 23:19:17 +02:00
if len(formsemestre.inscriptions) > 0:
2022-04-06 18:51:01 +02:00
H.append(
f"""<form name="f" method="get" action="{request.base_url}">
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
"""
)
2022-03-27 23:19:17 +02:00
if modejury:
H.append(
2022-04-06 18:51:01 +02:00
f'<input type="hidden" name="modejury" value="{modejury}"></input>'
2022-03-27 23:19:17 +02:00
)
2020-09-26 16:19:37 +02:00
H.append(
2022-03-27 23:19:17 +02:00
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
)
for (format, label) in (
("html", "Tableau"),
("evals", "Avec toutes les évaluations"),
("xml", "Bulletins XML (obsolète)"),
("json", "Bulletins JSON"),
2022-03-27 23:19:17 +02:00
):
if format == tabformat:
selected = " selected"
else:
selected = ""
2022-04-06 18:51:01 +02:00
H.append(f'<option value="{format}"{selected}>{label}</option>')
2022-03-27 23:19:17 +02:00
H.append("</select>")
H.append(
2022-04-06 18:51:01 +02:00
f"""&nbsp;(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>)
"""
2020-09-26 16:19:37 +02:00
)
data = do_formsemestre_recapcomplet(
formsemestre_id,
format=tabformat,
modejury=modejury,
sortcol=sortcol,
xml_with_decisions=xml_with_decisions,
rank_partition_id=rank_partition_id,
force_publishing=force_publishing,
2020-09-26 16:19:37 +02:00
)
if tabformat == "xml":
response = make_response(data)
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
H.append(data)
2020-09-26 16:19:37 +02:00
2022-04-06 18:51:01 +02:00
if not is_file:
2022-03-27 23:19:17 +02:00
if len(formsemestre.inscriptions) > 0:
H.append("</form>")
2020-09-26 16:19:37 +02:00
H.append(
2022-04-06 18:51:01 +02:00
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>"""
2020-09-26 16:19:37 +02:00
)
2022-03-27 23:19:17 +02:00
if sco_permissions_check.can_validate_sem(formsemestre_id):
H.append("<p>")
if modejury:
H.append(
2022-04-06 18:51:01 +02:00
f"""<a class="stdlink" href="{url_for('formsemestre_validation_auto',
formsemestre_id=formsemestre_id)
}">Calcul automatique des décisions du jury</a></p>"""
2022-03-27 23:19:17 +02:00
)
else:
H.append(
2022-04-06 18:51:01 +02:00
f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1)
}">Saisie des décisions du jury</a>"""
2022-03-27 23:19:17 +02:00
)
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
2020-09-26 16:19:37 +02:00
def do_formsemestre_recapcomplet(
formsemestre_id=None,
format="html", # html, xml, xls, xlsall, json
2020-09-26 16:19:37 +02:00
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,
2020-09-26 16:19:37 +02:00
):
"""Calcule et renvoie le tableau récapitulatif."""
2022-03-26 23:33:57 +01:00
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
2022-04-06 18:51:01 +02:00
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)
2022-04-06 18:51:01 +02:00
data = gen_formsemestre_recapcomplet_html(
formsemestre,
res,
include_evaluations=(format == "evals"),
filename=filename,
2022-03-26 23:33:57 +01:00
)
2020-09-26 16:19:37 +02:00
return data
2022-04-06 18:51:01 +02:00
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,
)
2022-04-06 18:51:01 +02:00
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,
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 format == "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)
2022-04-06 18:51:01 +02:00
raise ScoValueError(f"Format demandé invalide: {format}")
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."
2022-02-12 22:57:46 +01:00
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
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_id)
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)
:param force_publishing: donne les bulletins même si non "publiés sur portail"
2022-04-06 18:51:01 +02:00
:returns: dict
"""
2021-12-17 21:59:28 +01:00
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)
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"]
2022-02-12 22:57:46 +01:00
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]
2021-12-17 21:59:28 +01:00
if is_apc:
etud = Identite.query.get(etudid)
2021-12-24 00:08:25 +01:00
r = bulletin_but.BulletinBUT(formsemestre)
2021-12-17 21:59:28 +01:00
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,
)
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):
"""Tous les bulletins des semestres publiés 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)
log("formsemestres_bulletins(%s): %d sems" % (annee_scolaire, len(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_cell(key: str, row: dict, elt="td"):
"html table cell"
klass = row.get(f"_{key}_class")
attrs = f'class="{klass}"' if klass else ""
order = row.get(f"_{key}_order")
2022-03-26 23:33:57 +01:00
if order:
attrs += f' data-order="{order}"'
content = row.get(key, "")
target = row.get(f"_{key}_target")
target_attrs = row.get(f"_{key}_target_attrs", "")
if target or target_attrs: # avec lien
href = f'href="{target}"' if target else ""
content = f"<a {href} {target_attrs}>{content}</a>"
return f"<{elt} {attrs}>{content}</{elt}>"
2022-03-26 23:33:57 +01:00
def _gen_row(keys: list[str], row, elt="td"):
klass = row.get("_tr_class")
tr_class = f'class="{klass}"' if klass else ""
return f'<tr {tr_class}>{"".join([_gen_cell(key, row, elt) for key in keys])}</tr>'
def gen_formsemestre_recapcomplet_html(
2022-04-06 18:51:01 +02:00
formsemestre: FormSemestre,
res: NotesTableCompat,
include_evaluations=False,
filename="",
):
2022-03-26 23:33:57 +01:00
"""Construit table recap pour le BUT
2022-04-05 22:23:55 +02:00
Cache le résultat pour le semestre.
2022-03-26 23:33:57 +01:00
Return: data, filename
2022-04-06 18:51:01 +02:00
data est une chaine, le <div>...</div> incluant le tableau.
2022-03-26 23:33:57 +01:00
"""
2022-04-05 22:23:55 +02:00
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
)
if include_evaluations:
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
else:
sco_cache.TableRecapCache.set(formsemestre.id, table_html)
2022-04-06 18:51:01 +02:00
return table_html
2022-04-05 22:23:55 +02:00
def _gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre,
res: NotesTableCompat,
include_evaluations=False,
filename: str = "",
) -> str:
"""Génère le html"""
rows, footer_rows, titles, column_ids = res.get_table_recap(
convert_values=True, include_evaluations=include_evaluations
)
2022-03-27 23:19:17 +02:00
if not rows:
return (
2022-04-06 18:51:01 +02:00
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
2022-03-27 23:19:17 +02:00
)
H = [
f"""<div class="table_recap"><table class="table_recap {
'apc' if formsemestre.formation.is_apc() else 'classic'}"
data-filename="{filename}">"""
]
2022-03-26 23:33:57 +01:00
# header
H.append(
f"""
<thead>
{_gen_row(column_ids, titles, "th")}
</thead>
"""
)
# body
H.append("<tbody>")
for row in rows:
H.append(f"{_gen_row(column_ids, row)}\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'{_gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
2022-03-26 23:33:57 +01:00
H.append(
"""
2022-03-26 23:33:57 +01:00
</tfoot>
</table>
</div>
"""
)
2022-04-05 22:23:55 +02:00
return "".join(H)
2022-04-06 18:51:01 +02:00
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