972 lines
33 KiB
Python
972 lines
33 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Liste des notes d'une évaluation
|
|
"""
|
|
from collections import defaultdict
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
import flask
|
|
from flask import url_for, g, request
|
|
|
|
from app.comp import res_sem
|
|
from app.comp import moy_mod
|
|
from app.comp.moy_mod import ModuleImplResults
|
|
from app.comp.res_but import ResultatsSemestreBUT
|
|
from app.comp.res_compat import NotesTableCompat
|
|
from app.models import FormSemestre, Module
|
|
from app.models.etudiants import Identite
|
|
from app.models.evaluations import Evaluation
|
|
from app.models.moduleimpls import ModuleImpl
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
|
|
from app.scodoc.sco_etud import etud_sort_key
|
|
from app.scodoc import sco_evaluations
|
|
from app.scodoc import sco_evaluation_db
|
|
from app.scodoc import sco_groups
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc import sco_users
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc.gen_tables import GenTable
|
|
from app.scodoc.htmlutils import histogram_notes
|
|
import sco_version
|
|
|
|
|
|
def do_evaluation_listenotes(
|
|
evaluation_id=None, moduleimpl_id=None, fmt="html"
|
|
) -> tuple[str | flask.Response, str]:
|
|
"""
|
|
Affichage des notes d'une évaluation (si evaluation_id)
|
|
ou de toutes les évaluations d'un module (si moduleimpl_id)
|
|
"""
|
|
mode = None
|
|
evaluations: list[Evaluation] = []
|
|
if moduleimpl_id is not None:
|
|
mode = "module"
|
|
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
|
evaluations = modimpl.evaluations.all()
|
|
elif evaluation_id is not None:
|
|
mode = "eval"
|
|
evaluations = Evaluation.query.filter_by(id=evaluation_id).all()
|
|
else:
|
|
raise ValueError("missing argument: evaluation or module")
|
|
if not evaluations:
|
|
return "<p>Aucune évaluation !</p>", "ScoDoc"
|
|
evaluation = evaluations[0]
|
|
modimpl = evaluation.moduleimpl # il y a au moins une evaluation
|
|
|
|
# description de l'evaluation
|
|
if evaluation_id is not None:
|
|
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
|
|
page_title = f"Notes {evaluation.description or modimpl.module.code}"
|
|
else:
|
|
H = []
|
|
page_title = f"Notes {modimpl.module.code}"
|
|
# groupes
|
|
groups = sco_groups.do_evaluation_listegroupes(evaluation.id, include_default=True)
|
|
grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons
|
|
grnams = [str(g["group_id"]) for g in groups] # noms des checkbox
|
|
|
|
if len(evaluations) > 1:
|
|
descr = [
|
|
(
|
|
"moduleimpl_id",
|
|
{"default": modimpl.id, "input_type": "hidden"},
|
|
)
|
|
]
|
|
else:
|
|
descr = [
|
|
(
|
|
"evaluation_id",
|
|
{"default": evaluation.id, "input_type": "hidden"},
|
|
)
|
|
]
|
|
if len(grnams) > 1:
|
|
descr += [
|
|
(
|
|
"s",
|
|
{
|
|
"input_type": "separator",
|
|
"title": "<b>Choix du ou des groupes d'étudiants:</b>",
|
|
},
|
|
),
|
|
(
|
|
"group_ids",
|
|
{
|
|
"input_type": "checkbox",
|
|
"title": "",
|
|
"allowed_values": grnams,
|
|
"labels": grlabs,
|
|
"attributes": ('onclick="document.tf.submit();"',),
|
|
},
|
|
),
|
|
]
|
|
else:
|
|
if grnams:
|
|
def_nam = grnams[0]
|
|
else:
|
|
def_nam = ""
|
|
descr += [
|
|
(
|
|
"group_ids",
|
|
{"input_type": "hidden", "type": "list", "default": [def_nam]},
|
|
)
|
|
]
|
|
descr += [
|
|
(
|
|
"anonymous_listing",
|
|
{
|
|
"input_type": "checkbox",
|
|
"title": "",
|
|
"allowed_values": ("yes",),
|
|
"labels": ('listing "anonyme"',),
|
|
"attributes": ('onclick="document.tf.submit();"',),
|
|
"template": """<tr><td class="tf-fieldlabel">%(label)s</td>
|
|
<td class="tf-field">%(elem)s """,
|
|
},
|
|
),
|
|
(
|
|
"note_sur_20",
|
|
{
|
|
"input_type": "checkbox",
|
|
"title": "",
|
|
"allowed_values": ("yes",),
|
|
"labels": ("notes sur 20",),
|
|
"attributes": ('onclick="document.tf.submit();"',),
|
|
"template": "%(elem)s ",
|
|
},
|
|
),
|
|
(
|
|
"hide_groups",
|
|
{
|
|
"input_type": "checkbox",
|
|
"title": "",
|
|
"allowed_values": ("yes",),
|
|
"labels": ("masquer les groupes",),
|
|
"attributes": ('onclick="document.tf.submit();"',),
|
|
"template": "%(elem)s ",
|
|
},
|
|
),
|
|
(
|
|
"with_emails",
|
|
{
|
|
"input_type": "checkbox",
|
|
"title": "",
|
|
"allowed_values": ("yes",),
|
|
"labels": ("montrer les e-mails",),
|
|
"attributes": ('onclick="document.tf.submit();"',),
|
|
"template": "%(elem)s</td></tr>",
|
|
},
|
|
),
|
|
]
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
descr,
|
|
cancelbutton=None,
|
|
submitbutton=None,
|
|
bottom_buttons=False,
|
|
method="GET", # consultation
|
|
cssclass="noprint",
|
|
name="tf",
|
|
is_submitted=True, # toujours "soumis" (démarre avec liste complète)
|
|
)
|
|
if tf[0] == 0:
|
|
return "\n".join(H) + "\n" + tf[1], page_title
|
|
elif tf[0] == -1:
|
|
return (
|
|
flask.redirect(
|
|
url_for(
|
|
"notes.moduleimpl_status",
|
|
scodoc_dept=g.scodoc_dept,
|
|
moduleimpl_id=modimpl.id,
|
|
)
|
|
),
|
|
"",
|
|
)
|
|
else:
|
|
anonymous_listing = tf[2]["anonymous_listing"]
|
|
note_sur_20 = tf[2]["note_sur_20"]
|
|
hide_groups = tf[2]["hide_groups"]
|
|
with_emails = tf[2]["with_emails"]
|
|
group_ids = [x for x in tf[2]["group_ids"] if x != ""]
|
|
return (
|
|
_make_table_notes(
|
|
tf[1],
|
|
evaluations,
|
|
fmt=fmt,
|
|
note_sur_20=note_sur_20,
|
|
anonymous_listing=anonymous_listing,
|
|
group_ids=group_ids,
|
|
hide_groups=hide_groups,
|
|
with_emails=with_emails,
|
|
mode=mode,
|
|
),
|
|
page_title,
|
|
)
|
|
|
|
|
|
def _make_table_notes(
|
|
html_form,
|
|
evaluations: list[Evaluation],
|
|
fmt: str = "",
|
|
note_sur_20=False,
|
|
anonymous_listing=False,
|
|
hide_groups=False,
|
|
with_emails=False,
|
|
group_ids: list[int] | None = None,
|
|
mode="module", # "eval" or "module"
|
|
) -> str:
|
|
"""Table liste notes (une seule évaluation ou toutes celles d'un module)"""
|
|
group_ids = group_ids or []
|
|
if not evaluations:
|
|
return "<p>Aucune évaluation !</p>"
|
|
evaluation = evaluations[0]
|
|
modimpl = evaluation.moduleimpl
|
|
module: Module = modimpl.module
|
|
formsemestre: FormSemestre = modimpl.formsemestre
|
|
is_apc = module.formation.is_apc()
|
|
if is_apc:
|
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
|
|
is_conforme = modimpl.check_apc_conformity(res)
|
|
evals_poids, ues = moy_mod.load_evaluations_poids(modimpl.id)
|
|
if not ues:
|
|
is_apc = False
|
|
else:
|
|
evals_poids, ues = None, None
|
|
is_conforme = True
|
|
# (debug) check that all evals are in same module:
|
|
for e in evaluations:
|
|
if e.moduleimpl_id != modimpl.id:
|
|
raise ValueError("invalid evaluations list")
|
|
|
|
if fmt == "xls" or fmt == "json":
|
|
keep_numeric = True # pas de conversion des notes en strings
|
|
else:
|
|
keep_numeric = False
|
|
# Si pas de groupe, affiche tout
|
|
if not group_ids:
|
|
group_ids = [sco_groups.get_default_group(formsemestre.id)]
|
|
groups = sco_groups.listgroups(group_ids)
|
|
|
|
gr_title = sco_groups.listgroups_abbrev(groups)
|
|
gr_title_filename = sco_groups.listgroups_filename(groups)
|
|
|
|
if anonymous_listing:
|
|
columns_ids = ["code"] # cols in table
|
|
else:
|
|
if fmt in {"xls", "xml", "json"}:
|
|
columns_ids = ["etudid", "nom", "prenom"]
|
|
else:
|
|
columns_ids = ["nomprenom"]
|
|
if not hide_groups and fmt not in {"xml", "json"}:
|
|
# n'indique pas les groupes en xml et json car notation "humaine" ici
|
|
columns_ids.append("group")
|
|
|
|
titles = {
|
|
"code": "Code",
|
|
"group": "Groupe",
|
|
"nom": "Nom",
|
|
"prenom": "Prénom",
|
|
"nomprenom": "Nom",
|
|
"expl_key": "Rem.",
|
|
"email": "e-mail",
|
|
"emailperso": "e-mail perso",
|
|
"signatures": "Signatures",
|
|
}
|
|
rows = []
|
|
|
|
class KeyManager(dict):
|
|
"comment : key (pour regrouper les comments a la fin)"
|
|
|
|
def __init__(self):
|
|
self.lastkey = 1
|
|
|
|
def nextkey(self) -> str:
|
|
"get new key (int)"
|
|
r = self.lastkey
|
|
self.lastkey += 1
|
|
# self.lastkey = chr(ord(self.lastkey)+1)
|
|
return str(r)
|
|
|
|
key_mgr = KeyManager()
|
|
|
|
# code pour listings anonyme, à la place du nom
|
|
if sco_preferences.get_preference("anonymous_lst_code") == "INE":
|
|
anonymous_lst_key = "code_ine"
|
|
elif sco_preferences.get_preference("anonymous_lst_code") == "NIP":
|
|
anonymous_lst_key = "code_nip"
|
|
else:
|
|
anonymous_lst_key = "etudid"
|
|
|
|
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
|
|
evaluation.id, groups, include_demdef=True
|
|
)
|
|
for etudid, etat in etudid_etats:
|
|
css_row_class = None
|
|
# infos identite etudiant
|
|
etud: Identite = Identite.query.filter_by(
|
|
id=etudid, dept_id=g.scodoc_dept_id
|
|
).first()
|
|
if etud is None:
|
|
continue
|
|
|
|
if etat == scu.INSCRIT: # si inscrit, indique groupe
|
|
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
|
|
grc = sco_groups.listgroups_abbrev(groups)
|
|
else:
|
|
if etat == scu.DEMISSION:
|
|
grc = "DEM" # attention: ce code est re-ecrit plus bas, ne pas le changer (?)
|
|
css_row_class = "etuddem"
|
|
else:
|
|
grc = etat
|
|
|
|
code = getattr(etud, anonymous_lst_key)
|
|
if not code: # laisser le code vide n'aurait aucun sens, prenons l'etudid
|
|
code = etudid
|
|
|
|
rows.append(
|
|
{
|
|
"code": str(code), # INE, NIP ou etudid
|
|
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
|
|
"etudid": etudid,
|
|
"nom": etud.nom.upper(),
|
|
"_nomprenom_target": url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre.id,
|
|
etudid=etudid,
|
|
),
|
|
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{
|
|
etud.sort_key}" """,
|
|
"prenom": etud.prenom.lower().capitalize(),
|
|
"nom_usuel": etud.nom_usuel,
|
|
"nomprenom": etud.nomprenom,
|
|
"group": grc,
|
|
"_group_td_attrs": 'class="group"',
|
|
"email": etud.get_first_email(),
|
|
"emailperso": etud.get_first_email("emailperso"),
|
|
"_css_row_class": css_row_class or "",
|
|
}
|
|
)
|
|
|
|
# Lignes en tête:
|
|
row_coefs = {
|
|
"nom": "",
|
|
"prenom": "",
|
|
"nomprenom": "",
|
|
"group": "",
|
|
"code": "",
|
|
"_css_row_class": "sorttop fontitalic",
|
|
"_table_part": "head",
|
|
}
|
|
row_poids = {
|
|
"nom": "",
|
|
"prenom": "",
|
|
"nomprenom": "",
|
|
"group": "",
|
|
"code": "",
|
|
"_css_row_class": "sorttop poids",
|
|
"_table_part": "head",
|
|
}
|
|
row_note_max = {
|
|
"nom": "",
|
|
"prenom": "",
|
|
"nomprenom": "",
|
|
"group": "",
|
|
"code": "",
|
|
"_css_row_class": "sorttop fontitalic",
|
|
"_table_part": "head",
|
|
}
|
|
row_moys = {
|
|
"_css_row_class": "moyenne sortbottom",
|
|
"_table_part": "foot",
|
|
#'_nomprenom_td_attrs' : 'colspan="2" ',
|
|
"nomprenom": "Moyenne :",
|
|
"comment": "",
|
|
}
|
|
# Ajoute les notes de chaque évaluation:
|
|
evals_state: dict[int, dict] = {}
|
|
for e in evaluations:
|
|
evals_state[e.id] = sco_evaluations.do_evaluation_etat(e.id)
|
|
notes, nb_abs, nb_att = _add_eval_columns(
|
|
e,
|
|
evals_state[e.id],
|
|
evals_poids,
|
|
ues,
|
|
rows,
|
|
titles,
|
|
row_coefs,
|
|
row_poids,
|
|
row_note_max,
|
|
row_moys,
|
|
is_apc,
|
|
key_mgr,
|
|
note_sur_20,
|
|
keep_numeric,
|
|
fmt=fmt,
|
|
)
|
|
columns_ids.append(e.id)
|
|
#
|
|
if anonymous_listing:
|
|
rows.sort(key=lambda x: x["code"] or "")
|
|
else:
|
|
# sort by nom, prenom, sans accents
|
|
rows.sort(key=etud_sort_key)
|
|
|
|
# Si module, ajoute la (les) "moyenne(s) du module:
|
|
if mode == "module":
|
|
if len(evaluations) > 1:
|
|
# Moyenne de l'étudiant dans le module
|
|
# Affichée même en APC à titre indicatif
|
|
_add_moymod_column(
|
|
formsemestre.id,
|
|
modimpl.id,
|
|
rows,
|
|
columns_ids,
|
|
titles,
|
|
row_coefs,
|
|
row_poids,
|
|
row_note_max,
|
|
row_moys,
|
|
is_apc,
|
|
keep_numeric,
|
|
)
|
|
if is_apc:
|
|
# Ajoute une colonne par UE
|
|
_add_apc_columns(
|
|
modimpl,
|
|
evals_poids,
|
|
ues,
|
|
rows,
|
|
columns_ids,
|
|
titles,
|
|
is_conforme,
|
|
row_coefs,
|
|
row_poids,
|
|
row_note_max,
|
|
row_moys,
|
|
keep_numeric,
|
|
)
|
|
|
|
# Ajoute colonnes emails tout à droite:
|
|
if with_emails:
|
|
columns_ids += ["email", "emailperso"]
|
|
# Ajoute lignes en tête et moyennes
|
|
if len(evaluations) > 0 and fmt != "bordereau" and fmt != "json":
|
|
rows_head = [row_coefs]
|
|
if is_apc:
|
|
rows_head.append(row_poids)
|
|
rows_head.append(row_note_max)
|
|
rows = rows_head + rows
|
|
rows.append(row_moys)
|
|
# ajout liens HTMl vers affichage une evaluation:
|
|
if fmt == "html" and len(evaluations) > 1:
|
|
rlinks = {"_table_part": "head"}
|
|
for e in evaluations:
|
|
rlinks[e.id] = "afficher"
|
|
rlinks["_" + str(e.id) + "_help"] = (
|
|
"afficher seulement les notes de cette évaluation"
|
|
)
|
|
rlinks["_" + str(e.id) + "_target"] = url_for(
|
|
"notes.evaluation_listenotes",
|
|
scodoc_dept=g.scodoc_dept,
|
|
evaluation_id=e.id,
|
|
)
|
|
rlinks["_" + str(e.id) + "_td_attrs"] = ' class="tdlink" '
|
|
rows.append(rlinks)
|
|
|
|
if len(evaluations) == 1: # colonne "Rem." seulement si une eval
|
|
if fmt == "html": # pas d'indication d'origine en pdf (pour affichage)
|
|
columns_ids.append("expl_key")
|
|
elif fmt == "xls" or fmt == "xml":
|
|
columns_ids.append("comment")
|
|
elif fmt == "bordereau":
|
|
columns_ids.append("signatures")
|
|
|
|
# titres divers:
|
|
gl = "".join(["&group_ids%3Alist=" + str(g) for g in group_ids])
|
|
if note_sur_20:
|
|
gl = "¬e_sur_20%3Alist=yes" + gl
|
|
if anonymous_listing:
|
|
gl = "&anonymous_listing%3Alist=yes" + gl
|
|
if hide_groups:
|
|
gl = "&hide_groups%3Alist=yes" + gl
|
|
if with_emails:
|
|
gl = "&with_emails%3Alist=yes" + gl
|
|
if len(evaluations) == 1:
|
|
evalname = f"""{module.code}-{
|
|
evaluation.date_debut.replace(tzinfo=None).isoformat()
|
|
if evaluation.date_debut else ""}"""
|
|
hh = "%s, %s (%d étudiants)" % (
|
|
evaluation.description,
|
|
gr_title,
|
|
len(etudid_etats),
|
|
)
|
|
filename = scu.make_filename(f"notes_{evalname}_{gr_title_filename}")
|
|
|
|
if fmt == "bordereau":
|
|
hh = f""" {len(etudid_etats)} étudiants {
|
|
nb_abs} absent{'s' if nb_abs > 1 else ''}, {nb_att} en attente."""
|
|
# Attention: ReportLab supporte seulement '<br/>', pas '<br>' !
|
|
pdf_title = f"""<br/> BORDEREAU DE SIGNATURES
|
|
<br/><br/>{formsemestre.titre or ''}
|
|
<br/>({formsemestre.mois_debut()} - {formsemestre.mois_fin()})
|
|
semestre {formsemestre.semestre_id} {formsemestre.modalite or ""}
|
|
<br/>Notes du module {module.code} - {module.titre}
|
|
<br/>Évaluation : {evaluation.description}
|
|
"""
|
|
if evaluation.date_debut:
|
|
pdf_title += f" ({evaluation.date_debut.strftime('%d/%m/%Y')})"
|
|
pdf_title += "(noté sur {evaluation.note_max} )<br/><br/>"
|
|
else:
|
|
hh = " %s, %s (%d étudiants)" % (
|
|
evaluation.description,
|
|
gr_title,
|
|
len(etudid_etats),
|
|
)
|
|
if evaluation.date_debut:
|
|
pdf_title = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
|
|
else:
|
|
pdf_title = evaluation.description or f"évaluation dans {module.code}"
|
|
|
|
caption = hh
|
|
html_title = ""
|
|
base_url = (
|
|
url_for(
|
|
"notes.evaluation_listenotes",
|
|
scodoc_dept=g.scodoc_dept,
|
|
evaluation_id=evaluation.id,
|
|
)
|
|
+ gl
|
|
)
|
|
html_next_section = f"""<div class="notes_evaluation_stats">{nb_abs} absents,
|
|
{nb_att} en attente.</div>"""
|
|
else:
|
|
# Plusieurs évaluations (module)
|
|
filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename))
|
|
title = f"Notes {module.type_name()} {module.code} {module.titre}"
|
|
title += f""" semestre {formsemestre.titre_mois()}"""
|
|
if gr_title and gr_title != "tous":
|
|
title += " {gr_title}"
|
|
caption = title
|
|
html_next_section = ""
|
|
if fmt == "pdf" or fmt == "bordereau":
|
|
caption = "" # same as pdf_title
|
|
pdf_title = title
|
|
html_title = f"""<h2 class="formsemestre">Notes {module.type_name()}
|
|
<a class="stdlink" href="{
|
|
url_for("notes.moduleimpl_status",
|
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
|
|
}">{module.code} {module.titre}</a></h2>
|
|
"""
|
|
if not is_conforme:
|
|
html_title += (
|
|
"""<div class="warning">Poids des évaluations non conformes !</div>"""
|
|
)
|
|
base_url = (
|
|
url_for(
|
|
"notes.evaluation_listenotes",
|
|
scodoc_dept=g.scodoc_dept,
|
|
moduleimpl_id=modimpl.id,
|
|
)
|
|
+ gl
|
|
)
|
|
# display
|
|
tab = GenTable(
|
|
titles=titles,
|
|
columns_ids=columns_ids,
|
|
rows=rows,
|
|
html_sortable=True,
|
|
base_url=base_url,
|
|
filename=filename,
|
|
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
|
caption=caption,
|
|
html_next_section=html_next_section,
|
|
page_title="Notes de " + formsemestre.titre_mois(),
|
|
html_title=html_title,
|
|
pdf_title=pdf_title,
|
|
html_class="notes_evaluation",
|
|
preferences=sco_preferences.SemPreferences(formsemestre.id),
|
|
# html_generate_cells=False # la derniere ligne (moyennes) est incomplete
|
|
)
|
|
if fmt == "bordereau":
|
|
fmt = "pdf"
|
|
t = tab.make_page(fmt=fmt, with_html_headers=False)
|
|
if fmt != "html":
|
|
return t
|
|
|
|
if len(evaluations) > 1:
|
|
all_complete = True
|
|
for e in evaluations:
|
|
if not evals_state[e.id]["evalcomplete"]:
|
|
all_complete = False
|
|
if all_complete:
|
|
eval_info = """<span class="eval_info"><span class="eval_complete">Évaluations
|
|
prises en compte dans les moyennes.</span>"""
|
|
else:
|
|
eval_info = """<span class="eval_info help">
|
|
Les évaluations en vert et orange sont prises en compte dans les moyennes.
|
|
Celles en rouge n'ont pas toutes leurs notes."""
|
|
if is_apc:
|
|
eval_info += """ <span>La moyenne indicative est la moyenne des moyennes d'UE,
|
|
et n'est pas utilisée en BUT.
|
|
Les moyennes sur le groupe sont estimées sans les absents
|
|
(sauf pour les moyennes des moyennes d'UE) ni les démissionnaires.</span>"""
|
|
eval_info += """</span>"""
|
|
return html_form + eval_info + t + "<p></p>"
|
|
# Une seule evaluation: ajoute histogramme
|
|
histo = histogram_notes(notes)
|
|
# 2 colonnes: histo, comments
|
|
C = [
|
|
f"""<br><a class="stdlink" href="{base_url}&fmt=bordereau">Bordereau de Signatures (version PDF)</a>
|
|
<table>
|
|
<tr><td>
|
|
<div><h4>Répartition des notes:</h4>
|
|
{histo}
|
|
</div>
|
|
</td>
|
|
<td style="padding-left: 50px; vertical-align: top;"><p>
|
|
"""
|
|
]
|
|
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
|
|
commentkeys.sort(key=lambda x: int(x[1]))
|
|
for comment, key in commentkeys:
|
|
C.append(f"""<span class="colcomment">({key})</span> <em>{comment}</em><br>""")
|
|
if commentkeys:
|
|
C.append(
|
|
f"""<span><a class=stdlink" href="{ url_for(
|
|
'notes.evaluation_list_operations', scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id )
|
|
}">Gérer les opérations</a></span><br>
|
|
"""
|
|
)
|
|
eval_info = "xxx"
|
|
if evals_state[evaluation.id]["evalcomplete"]:
|
|
eval_info = '<span class="eval_info eval_complete">Evaluation prise en compte dans les moyennes</span>'
|
|
elif evals_state[evaluation.id]["evalattente"]:
|
|
eval_info = '<span class="eval_info eval_attente">Il y a des notes en attente (les autres sont prises en compte)</span>'
|
|
else:
|
|
eval_info = '<span class="eval_info eval_incomplete">Notes incomplètes, évaluation non prise en compte dans les moyennes</span>'
|
|
|
|
return (
|
|
sco_evaluations.evaluation_describe(evaluation_id=evaluation.id)
|
|
+ eval_info
|
|
+ html_form
|
|
+ t
|
|
+ "\n".join(C)
|
|
)
|
|
|
|
|
|
def _add_eval_columns(
|
|
evaluation: Evaluation,
|
|
eval_state,
|
|
evals_poids: pd.DataFrame | None,
|
|
ues,
|
|
rows,
|
|
titles,
|
|
row_coefs,
|
|
row_poids,
|
|
row_note_max,
|
|
row_moys,
|
|
is_apc,
|
|
K,
|
|
note_sur_20,
|
|
keep_numeric,
|
|
fmt="html",
|
|
):
|
|
"""Add eval e"""
|
|
nb_notes = 0
|
|
nb_abs = 0
|
|
nb_att = 0
|
|
sum_notes = 0
|
|
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
|
# actifs == inscrit au semestre, non DEM ni DEF:
|
|
_, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs()
|
|
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
|
|
|
|
if evaluation.date_debut:
|
|
titles[evaluation.id] = (
|
|
f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
|
|
)
|
|
else:
|
|
titles[evaluation.id] = f"{evaluation.description} "
|
|
|
|
if eval_state["evalcomplete"]:
|
|
klass = "eval_complete"
|
|
elif eval_state["evalattente"]:
|
|
klass = "eval_attente"
|
|
else:
|
|
klass = "eval_incomplete"
|
|
titles[evaluation.id] += " (non prise en compte)"
|
|
titles[f"_{evaluation.id}_td_attrs"] = f'class="{klass}"'
|
|
|
|
for row in rows:
|
|
etudid = row["etudid"]
|
|
if etudid in notes_db:
|
|
val = notes_db[etudid]["value"]
|
|
if val is None:
|
|
nb_abs += 1
|
|
if val == scu.NOTES_ATTENTE:
|
|
nb_att += 1
|
|
# calcul moyenne SANS LES ABSENTS ni les DEMISSIONNAIRES
|
|
if (
|
|
(etudid in etudids_actifs)
|
|
and val is not None
|
|
and val != scu.NOTES_NEUTRALISE
|
|
and val != scu.NOTES_ATTENTE
|
|
):
|
|
if evaluation.note_max > 0:
|
|
valsur20 = val * 20.0 / evaluation.note_max # remet sur 20
|
|
else:
|
|
valsur20 = 0
|
|
notes.append(valsur20) # toujours sur 20 pour l'histogramme
|
|
if note_sur_20:
|
|
val = valsur20 # affichage notes / 20 demandé
|
|
nb_notes = nb_notes + 1
|
|
sum_notes += val
|
|
val_fmt = scu.fmt_note(val, keep_numeric=keep_numeric)
|
|
comment = notes_db[etudid]["comment"]
|
|
if comment is None:
|
|
comment = ""
|
|
explanation = "%s (%s) %s" % (
|
|
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
|
|
sco_users.user_info(notes_db[etudid]["uid"])["nomcomplet"],
|
|
comment,
|
|
)
|
|
else:
|
|
if (etudid in etudids_actifs) and evaluation.publish_incomplete:
|
|
# Note manquante mais prise en compte immédiate: affiche ATT
|
|
val = scu.NOTES_ATTENTE
|
|
val_fmt = "ATT"
|
|
explanation = "non saisie mais prise en compte immédiate"
|
|
else:
|
|
explanation = ""
|
|
val_fmt = ""
|
|
val = None
|
|
|
|
cell_class = klass + {"ATT": " att", "ABS": " abs", "EXC": " exc"}.get(
|
|
val_fmt, ""
|
|
)
|
|
|
|
if val is None:
|
|
row[f"_{evaluation.id}_td_attrs"] = f'class="etudabs {cell_class}" '
|
|
if not row.get("_css_row_class", ""):
|
|
row["_css_row_class"] = "etudabs"
|
|
else:
|
|
row[f"_{evaluation.id}_td_attrs"] = f'class="{cell_class}" '
|
|
# regroupe les commentaires
|
|
if explanation:
|
|
if explanation in K:
|
|
expl_key = "(%s)" % K[explanation]
|
|
else:
|
|
K[explanation] = K.nextkey()
|
|
expl_key = "(%s)" % K[explanation]
|
|
else:
|
|
expl_key = ""
|
|
|
|
row.update(
|
|
{
|
|
evaluation.id: val_fmt,
|
|
"_" + str(evaluation.id) + "_help": explanation,
|
|
# si plusieurs evals seront ecrasés et non affichés:
|
|
"comment": explanation,
|
|
"expl_key": expl_key,
|
|
"_expl_key_help": explanation,
|
|
}
|
|
)
|
|
|
|
row_coefs[evaluation.id] = f"coef. {evaluation.coefficient:g}"
|
|
if is_apc:
|
|
if fmt == "html":
|
|
row_poids[evaluation.id] = _mini_table_eval_ue_poids(
|
|
evaluation.id, evals_poids, ues
|
|
)
|
|
else:
|
|
row_poids[evaluation.id] = evaluation.get_ue_poids_str()
|
|
if note_sur_20:
|
|
nmax = 20.0
|
|
else:
|
|
nmax = evaluation.note_max
|
|
if keep_numeric:
|
|
row_note_max[evaluation.id] = nmax
|
|
else:
|
|
row_note_max[evaluation.id] = f"/ {nmax}"
|
|
|
|
if nb_notes > 0:
|
|
row_moys[evaluation.id] = scu.fmt_note(
|
|
sum_notes / nb_notes, keep_numeric=keep_numeric
|
|
)
|
|
row_moys["_" + str(evaluation.id) + "_help"] = (
|
|
"moyenne sur %d notes (%s le %s)"
|
|
% (
|
|
nb_notes,
|
|
evaluation.description,
|
|
(
|
|
evaluation.date_debut.strftime("%d/%m/%Y")
|
|
if evaluation.date_debut
|
|
else ""
|
|
),
|
|
)
|
|
)
|
|
else:
|
|
row_moys[evaluation.id] = ""
|
|
|
|
return notes, nb_abs, nb_att # pour histogramme
|
|
|
|
|
|
def _mini_table_eval_ue_poids(
|
|
evaluation_id: int, evals_poids: pd.DataFrame, ues
|
|
) -> str:
|
|
"contenu de la cellule: poids"
|
|
ue_poids = [
|
|
(ue.acronyme, evals_poids[ue.id][evaluation_id])
|
|
for ue in ues
|
|
if (evals_poids[ue.id][evaluation_id] or 0) > 0
|
|
]
|
|
|
|
return (
|
|
"""<table class="eval_poids" title="poids vers les UE"><tr><td>"""
|
|
+ "</td><td>".join([f"{up[0]}" for up in ue_poids])
|
|
+ "</td></tr>"
|
|
+ "<tr><td>"
|
|
+ "</td><td>".join([f"{up[1]}" for up in ue_poids])
|
|
+ "</td></tr></table>"
|
|
)
|
|
|
|
|
|
def _add_moymod_column(
|
|
formsemestre_id,
|
|
moduleimpl_id,
|
|
rows,
|
|
columns_ids,
|
|
titles,
|
|
row_coefs,
|
|
row_poids,
|
|
row_note_max,
|
|
row_moys,
|
|
is_apc,
|
|
keep_numeric,
|
|
):
|
|
"""Ajoute la colonne moymod à rows"""
|
|
col_id = "moymod"
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
_, etudids_actifs = formsemestre.etudids_actifs()
|
|
nb_notes = 0
|
|
sum_notes = 0
|
|
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
|
for row in rows:
|
|
etudid = row["etudid"]
|
|
val = nt.get_etud_mod_moy(moduleimpl_id, etudid) # note sur 20, ou 'NA','NI'
|
|
row[col_id] = scu.fmt_note(val, keep_numeric=keep_numeric)
|
|
row["_" + col_id + "_td_attrs"] = ' class="moyenne" '
|
|
if etudid in etudids_actifs and not isinstance(val, str):
|
|
notes.append(val)
|
|
if not np.isnan(val):
|
|
nb_notes = nb_notes + 1
|
|
sum_notes += val
|
|
row_coefs[col_id] = "(avec abs)"
|
|
if is_apc:
|
|
row_poids[col_id] = "à titre indicatif"
|
|
if keep_numeric:
|
|
row_note_max[col_id] = 20.0
|
|
else:
|
|
row_note_max[col_id] = "/ 20"
|
|
titles[col_id] = "Moyenne module"
|
|
columns_ids.append(col_id)
|
|
if nb_notes > 0:
|
|
row_moys[col_id] = "%.3g" % (sum_notes / nb_notes)
|
|
row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
|
|
else:
|
|
row_moys[col_id] = ""
|
|
|
|
|
|
def _add_apc_columns(
|
|
modimpl,
|
|
evals_poids,
|
|
ues,
|
|
rows,
|
|
columns_ids,
|
|
titles,
|
|
is_conforme: bool,
|
|
row_coefs,
|
|
row_poids,
|
|
row_note_max,
|
|
row_moys,
|
|
keep_numeric,
|
|
):
|
|
"""Ajoute les colonnes moyennes vers les UE"""
|
|
# On raccorde ici les nouveaux calculs de notes (BUT 2021)
|
|
# sur l'ancien code ScoDoc
|
|
# => On recharge tout dans les nouveaux modèles
|
|
# rows est une liste de dict avec une clé "etudid"
|
|
# on va y ajouter une clé par UE du semestre
|
|
nt: ResultatsSemestreBUT = res_sem.load_formsemestre_results(modimpl.formsemestre)
|
|
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
|
|
_, etudids_actifs = modimpl.formsemestre.etudids_actifs()
|
|
# les UE dans lesquelles ce module a un coef non nul:
|
|
ues_with_coef = nt.modimpl_coefs_df[modimpl.id][
|
|
nt.modimpl_coefs_df[modimpl.id] > 0
|
|
].index
|
|
ues = [ue for ue in ues if ue.id in ues_with_coef]
|
|
sum_by_ue = defaultdict(float)
|
|
nb_notes_by_ue = defaultdict(int)
|
|
if is_conforme:
|
|
# valeur des moyennes vers les UEs:
|
|
for row in rows:
|
|
for ue in ues:
|
|
moy_ue = modimpl_results.etuds_moy_module[ue.id].get(row["etudid"], "?")
|
|
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
|
|
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
|
|
if (
|
|
isinstance(moy_ue, float)
|
|
and not np.isnan(moy_ue)
|
|
and row["etudid"] in etudids_actifs
|
|
):
|
|
sum_by_ue[ue.id] += moy_ue
|
|
nb_notes_by_ue[ue.id] += 1
|
|
# Nom et coefs des UE (lignes titres):
|
|
ue_coefs = modimpl.module.ue_coefs
|
|
if is_conforme:
|
|
coef_class = "coef_mod_ue"
|
|
else:
|
|
coef_class = "coef_mod_ue_non_conforme"
|
|
for ue in ues:
|
|
col_id = f"moy_ue_{ue.id}"
|
|
titles[col_id] = ue.acronyme
|
|
columns_ids.append(col_id)
|
|
coefs = [uc for uc in ue_coefs if uc.ue_id == ue.id]
|
|
if coefs:
|
|
row_coefs[f"moy_ue_{ue.id}"] = coefs[0].coef
|
|
row_coefs[f"_moy_ue_{ue.id}_td_attrs"] = f' class="{coef_class}" '
|
|
if nb_notes_by_ue[ue.id] > 0:
|
|
row_moys[col_id] = "%.3g" % (sum_by_ue[ue.id] / nb_notes_by_ue[ue.id])
|
|
row_moys["_" + col_id + "_help"] = "moyenne des moyennes"
|
|
else:
|
|
row_moys[col_id] = ""
|