forked from ScoDoc/ScoDoc
1078 lines
40 KiB
Python
1078 lines
40 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2022 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 json
|
|
import time
|
|
from xml.etree import ElementTree
|
|
|
|
from flask import g, request
|
|
from flask import make_response, url_for
|
|
|
|
from app import log
|
|
from app.but import bulletin_but
|
|
from app.comp import res_sem
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
from app.comp.res_common import NotesTableCompat
|
|
from app.models import FormSemestre
|
|
from app.models.etudiants import Identite
|
|
from app.models.evaluations import Evaluation
|
|
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_bac
|
|
from app.scodoc import sco_bulletins_json
|
|
from app.scodoc import sco_bulletins_xml
|
|
from app.scodoc import sco_bulletins, sco_excel
|
|
from app.scodoc import sco_codes_parcours
|
|
from app.scodoc import sco_evaluations
|
|
from app.scodoc import sco_evaluation_db
|
|
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
|
|
from app.scodoc import sco_xml
|
|
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.
|
|
"""
|
|
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")
|
|
H = []
|
|
if not isFile:
|
|
H += [
|
|
html_sco_header.sco_header(
|
|
page_title="Récapitulatif",
|
|
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
|
|
),
|
|
'<form name="f" method="get" action="%s">' % request.base_url,
|
|
'<input type="hidden" name="formsemestre_id" value="%s"></input>'
|
|
% formsemestre_id,
|
|
'<input type="hidden" name="pref_override" value="0"></input>',
|
|
]
|
|
if modejury:
|
|
H.append(
|
|
'<input type="hidden" name="modejury" value="%s"></input>' % modejury
|
|
)
|
|
H.append(
|
|
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
|
)
|
|
for (format, label) in (
|
|
("html", "HTML"),
|
|
("xls", "Fichier tableur (Excel)"),
|
|
("xlsall", "Fichier tableur avec toutes les évals"),
|
|
("csv", "Fichier tableur (CSV)"),
|
|
("xml", "Fichier XML"),
|
|
("json", "JSON"),
|
|
):
|
|
if format == tabformat:
|
|
selected = " selected"
|
|
else:
|
|
selected = ""
|
|
H.append('<option value="%s"%s>%s</option>' % (format, selected, label))
|
|
H.append("</select>")
|
|
|
|
H.append(
|
|
"""(cliquer sur un nom pour afficher son bulletin ou <a class="stdlink" href="%s/Notes/formsemestre_bulletins_pdf?formsemestre_id=%s">ici avoir le classeur papier</a>)"""
|
|
% (scu.ScoURL(), formsemestre_id)
|
|
)
|
|
if not parcours.UE_IS_MODULE:
|
|
H.append(
|
|
"""<input type="checkbox" name="hidemodules" value="1" onchange="document.f.submit()" """
|
|
)
|
|
if hidemodules:
|
|
H.append("checked")
|
|
H.append(""" >cacher les modules</input>""")
|
|
H.append(
|
|
"""<input type="checkbox" name="hidebac" value="1" onchange="document.f.submit()" """
|
|
)
|
|
if hidebac:
|
|
H.append("checked")
|
|
H.append(""" >cacher bac</input>""")
|
|
data = do_formsemestre_recapcomplet(
|
|
formsemestre_id,
|
|
format=tabformat,
|
|
hidemodules=hidemodules,
|
|
hidebac=hidebac,
|
|
modejury=modejury,
|
|
sortcol=sortcol,
|
|
xml_with_decisions=xml_with_decisions,
|
|
rank_partition_id=rank_partition_id,
|
|
force_publishing=force_publishing,
|
|
)
|
|
if tabformat == "xml":
|
|
response = make_response(data)
|
|
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
|
return response
|
|
H.append(data)
|
|
|
|
if not isFile:
|
|
H.append("</form>")
|
|
H.append(
|
|
"""<p><a class="stdlink" href="formsemestre_pvjury?formsemestre_id=%s">Voir les décisions du jury</a></p>"""
|
|
% formsemestre_id
|
|
)
|
|
if sco_permissions_check.can_validate_sem(formsemestre_id):
|
|
H.append("<p>")
|
|
if modejury:
|
|
H.append(
|
|
"""<a class="stdlink" href="formsemestre_validation_auto?formsemestre_id=%s">Calcul automatique des décisions du jury</a></p>"""
|
|
% (formsemestre_id,)
|
|
)
|
|
else:
|
|
H.append(
|
|
"""<a class="stdlink" href="formsemestre_recapcomplet?formsemestre_id=%s&modejury=1&hidemodules=1">Saisie des décisions du jury</a>"""
|
|
% formsemestre_id
|
|
)
|
|
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
|
|
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,
|
|
):
|
|
"""Calcule et renvoie le tableau récapitulatif."""
|
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
|
if (
|
|
formsemestre.formation.is_apc()
|
|
and format not in ("xml", "json")
|
|
and not modejury
|
|
):
|
|
data, filename = make_formsemestre_recapcomplet_apc(formsemestre, format=format)
|
|
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":
|
|
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
|
|
)
|
|
else:
|
|
raise ValueError("unknown format %s" % format)
|
|
|
|
|
|
def make_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, # 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(
|
|
formsemestre_id,
|
|
xml_nodate,
|
|
xml_with_decisions=xml_with_decisions,
|
|
force_publishing=force_publishing,
|
|
)
|
|
elif format == "json":
|
|
return _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
|
|
|
|
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 = [
|
|
"""
|
|
<script type="text/javascript">
|
|
function va_saisir(formsemestre_id, etudid) {
|
|
loc = 'formsemestre_validation_etud_form?formsemestre_id='+formsemestre_id+'&etudid='+etudid;
|
|
if (SORT_COLUMN_INDEX) {
|
|
loc += '&sortcol=' + SORT_COLUMN_INDEX;
|
|
}
|
|
loc += '#etudid' + etudid;
|
|
document.location=loc;
|
|
}
|
|
</script>
|
|
<table class="notes_recapcomplet gt_table_searchable compact" id="recapcomplet">
|
|
"""
|
|
]
|
|
if sortcol: # sort table using JS sorttable
|
|
H.append(
|
|
"""<script type="text/javascript">
|
|
</script>
|
|
"""
|
|
% (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("<thead>\n" + ligne_titres_head + "\n</thead>\n<tbody>\n")
|
|
if disable_etudlink:
|
|
etudlink = "%(name)s"
|
|
else:
|
|
etudlink = """<a
|
|
href="formsemestre_bulletinetud?formsemestre_id=%(formsemestre_id)s&etudid=%(etudid)s&version=selectedevals"
|
|
id="%(etudid)s" class="etudinfo">%(name)s</a>"""
|
|
ir = 0
|
|
nblines = len(F) - 1
|
|
for l in F[1:]:
|
|
etudid = l[-1]
|
|
if ir == nblines - 6:
|
|
H.append("</tbody>")
|
|
H.append("<tfoot>")
|
|
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 = '<tr class="%s sortbottom">' % styl
|
|
else:
|
|
el = etudlink % {
|
|
"formsemestre_id": formsemestre_id,
|
|
"etudid": etudid,
|
|
"name": l[1],
|
|
}
|
|
if ir % 2 == 0:
|
|
cells = '<tr class="recap_row_even" id="etudid%s">' % etudid
|
|
else:
|
|
cells = '<tr class="recap_row_odd" id="etudid%s">' % etudid
|
|
ir += 1
|
|
# XXX nsn = [ x.replace('NA', '-') for x in l[:-2] ]
|
|
# notes sans le NA:
|
|
nsn = l[:-2] # copy
|
|
for i in range(len(nsn)):
|
|
if nsn[i] == "NA":
|
|
nsn[i] = "-"
|
|
try:
|
|
order = int(nsn[0].split()[0])
|
|
except:
|
|
order = 99999
|
|
cells += (
|
|
f'<td class="recap_col" data-order="{order:05d}">{nsn[0]}</td>' # rang
|
|
)
|
|
cells += '<td class="recap_col">%s</td>' % el # nom etud (lien)
|
|
if not hidebac:
|
|
cells += '<td class="recap_col_bac">%s</td>' % nsn[2] # bac
|
|
idx_col_gr = 3
|
|
else:
|
|
idx_col_gr = 2
|
|
cells += '<td class="recap_col">%s</td>' % nsn[idx_col_gr] # group name
|
|
|
|
# Style si moyenne generale < barre
|
|
idx_col_moy = idx_col_gr + 1
|
|
cssclass = "recap_col_moy"
|
|
try:
|
|
if float(nsn[idx_col_moy]) < (parcours.BARRE_MOY - scu.NOTES_TOLERANCE):
|
|
cssclass = "recap_col_moy_inf"
|
|
except:
|
|
pass
|
|
cells += '<td class="%s">%s</td>' % (cssclass, nsn[idx_col_moy])
|
|
ue_number = 0
|
|
for i in range(idx_col_moy + 1, len(nsn)):
|
|
if i in ue_index:
|
|
cssclass = "recap_col_ue"
|
|
# grise si moy UE < barre
|
|
ue = ues[ue_number]
|
|
ue_number += 1
|
|
|
|
if (ir < (nblines - 4)) or (ir == nblines - 3):
|
|
try:
|
|
if float(nsn[i]) < parcours.get_barre_ue(
|
|
ue["type"]
|
|
): # NOTES_BARRE_UE
|
|
cssclass = "recap_col_ue_inf"
|
|
elif float(nsn[i]) >= parcours.NOTES_BARRE_VALID_UE:
|
|
cssclass = "recap_col_ue_val"
|
|
except:
|
|
pass
|
|
else:
|
|
cssclass = "recap_col"
|
|
if (
|
|
ir == nblines - 3
|
|
): # si moyenne generale module < barre ue, surligne:
|
|
try:
|
|
if float(nsn[i]) < parcours.get_barre_ue(ue["type"]):
|
|
cssclass = "recap_col_moy_inf"
|
|
except:
|
|
pass
|
|
cells += '<td class="%s">%s</td>' % (cssclass, nsn[i])
|
|
if modejury and etudid:
|
|
decision_sem = nt.get_etud_decision_sem(etudid)
|
|
if is_dem[etudid]:
|
|
code = "DEM"
|
|
act = ""
|
|
elif decision_sem:
|
|
code = decision_sem["code"]
|
|
act = "(modifier)"
|
|
else:
|
|
code = ""
|
|
act = "saisir"
|
|
cells += '<td class="decision">%s' % code
|
|
if act:
|
|
# cells += ' <a href="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">%s</a>' % (formsemestre_id, etudid, act)
|
|
cells += (
|
|
""" <a href="#" onclick="va_saisir('%s', '%s')">%s</a>"""
|
|
% (formsemestre_id, etudid, act)
|
|
)
|
|
cells += "</td>"
|
|
H.append(cells + "</tr>")
|
|
|
|
H.append(ligne_titres_foot)
|
|
H.append("</tfoot>")
|
|
H.append("</table>")
|
|
|
|
# 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(
|
|
'<input type="radio" name="rank_partition_id" value="" onchange="document.f.submit()" %s/>tous '
|
|
% (checked)
|
|
)
|
|
for p in partitions:
|
|
if p["partition_id"] == rank_partition_id:
|
|
checked = "checked"
|
|
else:
|
|
checked = ""
|
|
H.append(
|
|
'<input type="radio" name="rank_partition_id" value="%s" onchange="document.f.submit()" %s/>%s '
|
|
% (p["partition_id"], checked, p["partition_name"])
|
|
)
|
|
|
|
# recap des decisions jury (nombre dans chaque code):
|
|
if codes_nb:
|
|
H.append("<h4>Décisions du jury</h4><table>")
|
|
cods = list(codes_nb.keys())
|
|
cods.sort()
|
|
for cod in cods:
|
|
H.append("<tr><td>%s</td><td>%d</td></tr>" % (cod, codes_nb[cod]))
|
|
H.append("</table>")
|
|
# Avertissements
|
|
if formsemestre.formation.is_apc():
|
|
H.append(
|
|
"""<p class="help">Pour les formations par compétences (comme le BUT), la moyenne générale est purement indicative et ne devrait pas être communiquée aux étudiants.</p>"""
|
|
)
|
|
return "\n".join(H), "", "html"
|
|
elif format == "csv":
|
|
CSV = scu.CSV_LINESEP.join(
|
|
[scu.CSV_FIELDSEP.join([str(x) for x in l]) for l in F]
|
|
)
|
|
semname = sem["titre_num"].replace(" ", "_")
|
|
date = time.strftime("%d-%m-%Y")
|
|
filename = "notes_modules-%s-%s.csv" % (semname, date)
|
|
return CSV, filename, "csv"
|
|
elif format[:3] == "xls":
|
|
semname = sem["titre_num"].replace(" ", "_")
|
|
date = time.strftime("%d-%m-%Y")
|
|
if format == "xls":
|
|
filename = "notes_modules-%s-%s%s" % (semname, date, scu.XLSX_SUFFIX)
|
|
else:
|
|
filename = "notes_modules_evals-%s-%s%s" % (semname, date, scu.XLSX_SUFFIX)
|
|
sheet_name = "notes %s %s" % (semname, date)
|
|
if len(sheet_name) > 31:
|
|
sheet_name = "notes %s %s" % ("...", date)
|
|
xls = sco_excel.excel_simple_table(
|
|
titles=["etudid", "code_nip"] + F[0][:-2],
|
|
lines=[
|
|
[x[-1], x[-2]] + x[:-2] for x in F[1:]
|
|
], # reordonne cols (etudid et nip en 1er),
|
|
sheet_name=sheet_name,
|
|
)
|
|
return xls, filename, "xls"
|
|
else:
|
|
raise ValueError("unknown format %s" % format)
|
|
|
|
|
|
def _ligne_titres(ue_index, F, cod2mod, modejury, with_modules_links=True):
|
|
"""Cellules de la ligne de titre (haut ou bas)"""
|
|
cells = '<tr class="recap_row_tit sortbottom" id="recap_trtit">'
|
|
for i in range(len(F[0]) - 2):
|
|
if i in ue_index:
|
|
cls = "recap_tit_ue"
|
|
else:
|
|
cls = "recap_tit"
|
|
attr = f'class="{cls}"'
|
|
if i == 0 or F[0][i] == "classement": # Rang: force tri numerique
|
|
try:
|
|
order = int(F[0][i].split()[0])
|
|
except:
|
|
order = 99999
|
|
attr += f' data-order="{order:05d}"'
|
|
if F[0][i] in cod2mod: # lien vers etat module
|
|
modimpl = cod2mod[F[0][i]]
|
|
if with_modules_links:
|
|
href = url_for(
|
|
"notes.moduleimpl_status",
|
|
scodoc_dept=g.scodoc_dept,
|
|
moduleimpl_id=modimpl.id,
|
|
)
|
|
else:
|
|
href = ""
|
|
cells += f"""<td {attr}><a href="{href}" title="{modimpl.module.titre} ({
|
|
sco_users.user_info(modimpl.responsable_id)["nomcomplet"]})">{F[0][i]}</a></td>"""
|
|
else:
|
|
cells += f"<td {attr}>{F[0][i]}</td>"
|
|
if modejury:
|
|
cells += '<td class="recap_tit">Décision</td>'
|
|
return cells + "</tr>"
|
|
|
|
|
|
def _list_notes_evals(evals: list[Evaluation], etudid: int) -> list[str]:
|
|
"""Liste des notes des evaluations completes de ce module
|
|
(pour table xls avec evals)
|
|
"""
|
|
L = []
|
|
for e in evals:
|
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(e.evaluation_id)
|
|
if etudid in notes_db:
|
|
val = notes_db[etudid]["value"]
|
|
else:
|
|
# Note manquante mais prise en compte immédiate: affiche ATT
|
|
val = scu.NOTES_ATTENTE
|
|
val_fmt = scu.fmt_note(val, keep_numeric=True)
|
|
L.append(val_fmt)
|
|
return L
|
|
|
|
|
|
def _list_notes_evals_titles(codemodule: str, evals: list[Evaluation]) -> list[str]:
|
|
"""Liste des titres des evals completes"""
|
|
L = []
|
|
eval_index = len(evals) - 1
|
|
for e in evals:
|
|
L.append(
|
|
codemodule
|
|
+ "-"
|
|
+ str(eval_index)
|
|
+ "-"
|
|
+ (e.jour.isoformat() if e.jour else "")
|
|
)
|
|
eval_index -= 1
|
|
return L
|
|
|
|
|
|
def _list_notes_evals_stats(evals: list[Evaluation], key: str) -> list[str]:
|
|
"""Liste des stats (moy, ou rien!) des evals completes"""
|
|
L = []
|
|
for e in evals:
|
|
if key == "moy":
|
|
# TODO #sco92
|
|
# val = e["etat"]["moy_num"]
|
|
# L.append(scu.fmt_note(val, keep_numeric=True))
|
|
L.append("")
|
|
elif key == "max":
|
|
L.append(e.note_max)
|
|
elif key == "min":
|
|
L.append(0.0)
|
|
elif key == "coef":
|
|
L.append(e.coefficient)
|
|
else:
|
|
L.append("") # on n'a pas sous la main min/max
|
|
return L
|
|
|
|
|
|
def _formsemestre_recapcomplet_xml(
|
|
formsemestre_id,
|
|
xml_nodate,
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
):
|
|
"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 (
|
|
sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING),
|
|
"",
|
|
"xml",
|
|
)
|
|
|
|
|
|
def _formsemestre_recapcomplet_json(
|
|
formsemestre_id,
|
|
xml_nodate=False,
|
|
xml_with_decisions=False,
|
|
force_publishing=True,
|
|
):
|
|
"""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, "", "json"
|
|
"""
|
|
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)
|
|
J = {
|
|
"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 = J["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 J, "", "json"
|
|
|
|
|
|
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
|
|
"""
|
|
jslist = []
|
|
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:
|
|
J, _, _ = _formsemestre_recapcomplet_json(
|
|
sem["formsemestre_id"], force_publishing=False
|
|
)
|
|
jslist.append(J)
|
|
|
|
return scu.sendJSON(jslist)
|
|
|
|
|
|
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")
|
|
if order:
|
|
attrs += f' data-order="{order}"'
|
|
content = row.get(key, "")
|
|
target = row.get(f"_{key}_target")
|
|
if content or target: # avec lien
|
|
href = f'href="{target}"' if target else ""
|
|
content = f'<a {href} {row.get(f"_{key}_target_attrs", "")}>{content}</a>'
|
|
return f"<{elt} {attrs}>{content}</{elt}>"
|
|
|
|
|
|
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 make_formsemestre_recapcomplet_apc(formsemestre: FormSemestre, format="html"):
|
|
"""Construit table recap pour le BUT
|
|
Return: data, filename
|
|
"""
|
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
|
rows, footer_rows, titles, column_ids = res.get_table_moyennes_triees(
|
|
convert_values=True
|
|
)
|
|
H = ['<div class="table_recap"><table class="table_recap">']
|
|
# 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')
|
|
H.append(
|
|
"""
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
"""
|
|
)
|
|
return (
|
|
"".join(H),
|
|
f'recap-{formsemestre.titre_num().replace(" ", "_")}-{time.strftime("%d-%m-%Y")}',
|
|
) # suffix ?
|