Bulletins: evals rat. et sessions 2. Fix #492.

This commit is contained in:
Emmanuel Viennet 2022-09-18 16:53:00 +02:00
parent 219a3c2811
commit acd1b36d72
10 changed files with 70 additions and 48 deletions

View File

@ -13,7 +13,7 @@ import numpy as np
from flask import g, has_request_context, url_for from flask import g, has_request_context, url_for
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import Evaluation, FormSemestre, Identite
from app.models.groups import GroupDescr from app.models.groups import GroupDescr
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
@ -249,7 +249,7 @@ class BulletinBUT:
} }
return d return d
def etud_eval_results(self, etud, e) -> dict: def etud_eval_results(self, etud, e: Evaluation) -> dict:
"dict resultats d'un étudiant à une évaluation" "dict resultats d'un étudiant à une évaluation"
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits # eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id] eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
@ -265,12 +265,14 @@ class BulletinBUT:
poids = collections.defaultdict(lambda: 0.0) poids = collections.defaultdict(lambda: 0.0)
d = { d = {
"id": e.id, "id": e.id,
"description": e.description, "coef": fmt_note(e.coefficient)
if e.evaluation_type == scu.EVALUATION_NORMALE
else None,
"date": e.jour.isoformat() if e.jour else None, "date": e.jour.isoformat() if e.jour else None,
"description": e.description,
"evaluation_type": e.evaluation_type,
"heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None, "heure_debut": e.heure_debut.strftime("%H:%M") if e.heure_debut else None,
"heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None, "heure_fin": e.heure_fin.strftime("%H:%M") if e.heure_debut else None,
"coef": fmt_note(e.coefficient),
"poids": poids,
"note": { "note": {
"value": fmt_note( "value": fmt_note(
eval_notes[etud.id], eval_notes[etud.id],
@ -280,6 +282,7 @@ class BulletinBUT:
"max": fmt_note(notes_ok.max()), "max": fmt_note(notes_ok.max()),
"moy": fmt_note(notes_ok.mean()), "moy": fmt_note(notes_ok.mean()),
}, },
"poids": poids,
"url": url_for( "url": url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,

View File

@ -13,6 +13,7 @@ from reportlab.platypus import Paragraph, Spacer
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import gen_tables from app.scodoc import gen_tables
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc import sco_utils as scu
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard): class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
@ -312,18 +313,19 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
] ]
return col_keys, rows, pdf_style, col_widths return col_keys, rows, pdf_style, col_widths
def evaluations_rows(self, rows, evaluations, ue_acros=()): def evaluations_rows(self, rows, evaluations: list[dict], ue_acros=()):
"lignes des évaluations" "lignes des évaluations"
for e in evaluations: for e in evaluations:
coef = e["coef"] if e["evaluation_type"] == scu.EVALUATION_NORMALE else "*"
t = { t = {
"titre": f"{e['description'] or ''}", "titre": f"{e['description'] or ''}",
"moyenne": e["note"]["value"], "moyenne": e["note"]["value"],
"_moyenne_pdf": Paragraph( "_moyenne_pdf": Paragraph(
f"""<para align=right>{e["note"]["value"]}</para>""" f"""<para align=right>{e["note"]["value"]}</para>"""
), ),
"coef": e["coef"], "coef": coef,
"_coef_pdf": Paragraph( "_coef_pdf": Paragraph(
f"<para align=right fontSize={self.small_fontsize}><i>{e['coef']}</i></para>" f"<para align=right fontSize={self.small_fontsize}><i>{coef}</i></para>"
), ),
"_pdf_style": [ "_pdf_style": [
( (

View File

@ -90,7 +90,9 @@ def pvjury_table_but(formsemestre_id: int, format="html"):
if deca if deca
else "-", else "-",
"decision_but": deca.code_valide if deca else "", "decision_but": deca.code_valide if deca else "",
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]), "devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
if deca
else "",
} }
rows.append(row) rows.append(row)

View File

@ -124,7 +124,7 @@ class ModuleImplResults:
Évaluation "complete" (prise en compte dans les calculs) si: Évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes - soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise en compte immédiate" (publish_incomplete) - soit elle a été déclarée "à prise en compte immédiate" (publish_incomplete)
ou est une évaluation de rattrapage ou de session 2
Évaluation "attente" (prise en compte dans les calculs, mais il y Évaluation "attente" (prise en compte dans les calculs, mais il y
manque des notes) ssi il y a des étudiants inscrits au semestre et au module manque des notes) ssi il y a des étudiants inscrits au semestre et au module
qui ont des notes ATT. qui ont des notes ATT.
@ -148,11 +148,14 @@ class ModuleImplResults:
eval_df = self._load_evaluation_notes(evaluation) eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note # is_complete ssi tous les inscrits (non dem) au semestre ont une note
# ou évaluation déclarée "à prise en compte immédiate" # ou évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes # Les évaluations de rattrapage et 2eme session sont toujours complètes
# car on calcule leur moyenne à part.
etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem. etudids_sans_note = inscrits_module - set(eval_df.index) # sans les dem.
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and ( is_complete = (
evaluation.publish_incomplete or (not etudids_sans_note) (evaluation.evaluation_type == scu.EVALUATION_RATTRAPAGE)
or (evaluation.evaluation_type == scu.EVALUATION_SESSION2)
or (evaluation.publish_incomplete)
or (not etudids_sans_note)
) )
self.evaluations_completes.append(is_complete) self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete self.evaluations_completes_dict[evaluation.id] = is_complete

View File

@ -30,6 +30,7 @@
""" """
import email import email
import time import time
import numpy as np
from flask import g, request from flask import g, request
from flask import flash, jsonify, render_template, url_for from flask import flash, jsonify, render_template, url_for
@ -575,18 +576,20 @@ def _ue_mod_bulletin(
else: else:
e["name"] = "Points de bonus sur cette UE" e["name"] = "Points de bonus sur cette UE"
else: else:
e["name"] = e["description"] or "le %s" % e["jour"] e["name"] = e["description"] or f"le {e['jour']}"
e["target_html"] = ( e["target_html"] = url_for(
"evaluation_listenotes?evaluation_id=%s&format=html&tf_submitted=1" "notes.evaluation_listenotes",
% e["evaluation_id"] scodoc_dept=g.scodoc_dept,
evaluation_id=e["evaluation_id"],
format="html",
tf_submitted=1,
) )
e["name_html"] = '<a class="bull_link" href="%s">%s</a>' % ( e[
e["target_html"], "name_html"
e["name"], ] = f"""<a class="bull_link" href="{
) e['target_html']}">{e['name']}</a>"""
val = e["notes"].get(etudid, {"value": "NP"})[ val = e["notes"].get(etudid, {"value": "NP"})["value"]
"value" # val est NP si etud demissionnaire
] # NA si etud demissionnaire
if val == "NP": if val == "NP":
e["note_txt"] = "nd" e["note_txt"] = "nd"
e["note_html"] = '<span class="note_nd">nd</span>' e["note_html"] = '<span class="note_nd">nd</span>'
@ -604,13 +607,18 @@ def _ue_mod_bulletin(
if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE: if e["evaluation_type"] == scu.EVALUATION_RATTRAPAGE:
e["coef_txt"] = "rat." e["coef_txt"] = "rat."
elif e["evaluation_type"] == scu.EVALUATION_SESSION2: elif e["evaluation_type"] == scu.EVALUATION_SESSION2:
e["coef_txt"] = "sess. 2" e["coef_txt"] = "Ses. 2"
if e["etat"]["evalattente"]: if e["etat"]["evalattente"]:
mod_attente = True # une eval en attente dans ce module mod_attente = True # une eval en attente dans ce module
if (not is_malus) or (val != "NP"): if ((not is_malus) or (val != "NP")) and (
mod["evaluations"].append( (
e e["evaluation_type"] == scu.EVALUATION_NORMALE
) # ne liste pas les eval malus sans notes or not np.isnan(val)
)
):
# ne liste pas les eval malus sans notes
# ni les rattrapages et sessions 2 si pas de note
mod["evaluations"].append(e)
# Evaluations incomplètes ou futures: # Evaluations incomplètes ou futures:
mod["evaluations_incompletes"] = [] mod["evaluations_incompletes"] = []

View File

@ -150,7 +150,7 @@ class ScoInvalidIdType(ScoValueError):
</p> </p>
<p> Si le problème persiste, merci de contacter l'assistance <p> Si le problème persiste, merci de contacter l'assistance
via la liste de diffusion <a href="{scu.SCO_USERS_LIST}">Notes</a> via la liste de diffusion <a href="{scu.SCO_USERS_LIST}">Notes</a>
ou le salon Discord. ou de préférence le <a href="{scu.SCO_DISCORD_ASSISTANCE}">salon Discord</a>.
</p> </p>
<p>Message serveur: <tt>{msg}</tt></p> <p>Message serveur: <tt>{msg}</tt></p>
""" """

View File

@ -40,12 +40,14 @@ from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Module from app.models import Module
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError from app.scodoc.sco_exceptions import (
ScoValueError,
ScoInvalidDateError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -58,7 +60,6 @@ from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
@ -1008,6 +1009,10 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
def formsemestre_status(formsemestre_id=None): def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML""" """Tableau de bord semestre HTML"""
# porté du DTML # porté du DTML
if formsemestre_id is not None and not isinstance(formsemestre_id, int):
raise ScoInvalidIdType(
"formsemestre_bulletinetud: formsemestre_id must be an integer !"
)
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id

View File

@ -740,7 +740,8 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
_ADM_PATTERN = re.compile(r"[\W]+", re.UNICODE) # supprime tout sauf alphanum _ADM_PATTERN = re.compile(r"[\W]+", re.UNICODE) # supprime tout sauf alphanum
def adm_normalize_string(s): # normalize unicode title def adm_normalize_string(s):
"normalize unicode title"
return scu.suppress_accents(_ADM_PATTERN.sub("", s.strip().lower())).replace( return scu.suppress_accents(_ADM_PATTERN.sub("", s.strip().lower())).replace(
"_", "" "_", ""
) )
@ -750,16 +751,15 @@ def adm_get_fields(titles, formsemestre_id):
"""Cherche les colonnes importables dans les titres (ligne 1) du fichier excel """Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
return: { idx : (field_name, convertor) } return: { idx : (field_name, convertor) }
""" """
# log('adm_get_fields: titles=%s' % titles) format_dict = sco_import_format_dict()
Fmt = sco_import_format_dict()
fields = {} fields = {}
idx = 0 idx = 0
for title in titles: for title in titles:
title_n = adm_normalize_string(title) title_n = adm_normalize_string(title)
for k in Fmt: for k in format_dict:
for v in Fmt[k]["aliases"]: for v in format_dict[k]["aliases"]:
if adm_normalize_string(v) == title_n: if adm_normalize_string(v) == title_n:
typ = Fmt[k]["type"] typ = format_dict[k]["type"]
if typ == "real": if typ == "real":
convertor = adm_convert_real convertor = adm_convert_real
elif typ == "integer" or typ == "int": elif typ == "integer" or typ == "int":
@ -769,10 +769,9 @@ def adm_get_fields(titles, formsemestre_id):
# doublons ? # doublons ?
if k in [x[0] for x in fields.values()]: if k in [x[0] for x in fields.values()]:
raise ScoFormatError( raise ScoFormatError(
'scolars_import_admission: titre "%s" en double (ligne 1)' f"""scolars_import_admission: titre "{title}" en double (ligne 1)""",
% (title),
dest_url=url_for( dest_url=url_for(
"scolar.form_students_import_infos_admissions_apb", "scolar.form_students_import_infos_admissions",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), ),

View File

@ -399,7 +399,7 @@ class releveBUT extends HTMLElement {
<div>${module.titre} - ${evaluation.description || "Note"}</div> <div>${module.titre} - ${evaluation.description || "Note"}</div>
<div> <div>
${evaluation.note.value ?? "-"} ${evaluation.note.value ?? "-"}
<em>Coef.&nbsp;${evaluation.coef}</em> <em>Coef.&nbsp;${evaluation.coef ?? "*"}</em>
</div> </div>
</div> </div>
`; `;
@ -450,7 +450,7 @@ class releveBUT extends HTMLElement {
<div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div> <div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div>
<div> <div>
${evaluation.note.value} ${evaluation.note.value}
<em>Coef.&nbsp;${evaluation.coef}</em> <em>Coef.&nbsp;${evaluation.coef ?? "*"}</em>
</div> </div>
<div class=complement> <div class=complement>
<div>Coef</div><div>${evaluation.coef}</div> <div>Coef</div><div>${evaluation.coef}</div>

View File

@ -265,7 +265,7 @@ def formsemestre_bulletinetud(
): ):
format = format or "html" format = format or "html"
if not isinstance(formsemestre_id, int): if formsemestre_id is not None and not isinstance(formsemestre_id, int):
raise ScoInvalidIdType( raise ScoInvalidIdType(
"formsemestre_bulletinetud: formsemestre_id must be an integer !" "formsemestre_bulletinetud: formsemestre_id must be an integer !"
) )
@ -1775,7 +1775,7 @@ def evaluation_listenotes():
if "moduleimpl_id" in vals and vals["moduleimpl_id"]: if "moduleimpl_id" in vals and vals["moduleimpl_id"]:
moduleimpl_id = int(vals["moduleimpl_id"]) moduleimpl_id = int(vals["moduleimpl_id"])
except ValueError as exc: except ValueError as exc:
raise ScoValueError("adresse invalide !") from exc raise ScoValueError("evaluation_listenotes: id invalides !") from exc
format = vals.get("format", "html") format = vals.get("format", "html")
html_content, page_title = sco_liste_notes.do_evaluation_listenotes( html_content, page_title = sco_liste_notes.do_evaluation_listenotes(