Evaluations: modernisation code

This commit is contained in:
Emmanuel Viennet 2023-12-10 20:59:32 +01:00
parent 0b6f60897b
commit a4fbc2b80e
14 changed files with 322 additions and 298 deletions

View File

@ -408,7 +408,7 @@ class NotesTableCompat(ResultatsSemestre):
de ce module. de ce module.
Évaluation "complete" ssi toutes notes saisies ou en attente. Évaluation "complete" ssi toutes notes saisies ou en attente.
""" """
modimpl = db.session.get(ModuleImpl, moduleimpl_id) modimpl: ModuleImpl = db.session.get(ModuleImpl, moduleimpl_id)
modimpl_results = self.modimpls_results.get(moduleimpl_id) modimpl_results = self.modimpls_results.get(moduleimpl_id)
if not modimpl_results: if not modimpl_results:
return [] # safeguard return [] # safeguard

View File

@ -12,9 +12,7 @@ import sqlalchemy as sa
from app import db, log from app import db, log
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.events import ScolarNews from app.models.events import ScolarNews
from app.models.moduleimpls import ModuleImpl
from app.models.notes import NotesNotes from app.models.notes import NotesNotes
from app.models.ues import UniteEns
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -67,7 +65,7 @@ class Evaluation(db.Model):
@classmethod @classmethod
def create( def create(
cls, cls,
moduleimpl: ModuleImpl = None, moduleimpl: "ModuleImpl" = None,
date_debut: datetime.datetime = None, date_debut: datetime.datetime = None,
date_fin: datetime.datetime = None, date_fin: datetime.datetime = None,
description=None, description=None,
@ -114,7 +112,7 @@ class Evaluation(db.Model):
@classmethod @classmethod
def get_new_numero( def get_new_numero(
cls, moduleimpl: ModuleImpl, date_debut: datetime.datetime cls, moduleimpl: "ModuleImpl", date_debut: datetime.datetime
) -> int: ) -> int:
"""Get a new numero for an evaluation in this moduleimpl """Get a new numero for an evaluation in this moduleimpl
If necessary, renumber existing evals to make room for a new one. If necessary, renumber existing evals to make room for a new one.
@ -145,7 +143,7 @@ class Evaluation(db.Model):
"delete evaluation (commit) (check permission)" "delete evaluation (commit) (check permission)"
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
modimpl: ModuleImpl = self.moduleimpl modimpl: "ModuleImpl" = self.moduleimpl
if not modimpl.can_edit_evaluation(current_user): if not modimpl.can_edit_evaluation(current_user):
raise AccessDenied( raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}" f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
@ -239,7 +237,7 @@ class Evaluation(db.Model):
check_convert_evaluation_args(self.moduleimpl, data) check_convert_evaluation_args(self.moduleimpl, data)
if data.get("numero") is None: if data.get("numero") is None:
data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1 data["numero"] = Evaluation.get_max_numero(self.moduleimpl.id) + 1
for k in self.__dict__.keys(): for k in self.__dict__:
if k != "_sa_instance_state" and k != "id" and k in data: if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k]) setattr(self, k, data[k])
@ -257,7 +255,7 @@ class Evaluation(db.Model):
@classmethod @classmethod
def moduleimpl_evaluation_renumber( def moduleimpl_evaluation_renumber(
cls, moduleimpl: ModuleImpl, only_if_unumbered=False cls, moduleimpl: "ModuleImpl", only_if_unumbered=False
): ):
"""Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one) """Renumber evaluations in this moduleimpl, according to their date. (numero=0: oldest one)
Needed because previous versions of ScoDoc did not have eval numeros Needed because previous versions of ScoDoc did not have eval numeros
@ -394,6 +392,8 @@ class Evaluation(db.Model):
"""set poids vers les UE (remplace existants) """set poids vers les UE (remplace existants)
ue_poids_dict = { ue_id : poids } ue_poids_dict = { ue_id : poids }
""" """
from app.models.ues import UniteEns
L = [] L = []
for ue_id, poids in ue_poids_dict.items(): for ue_id, poids in ue_poids_dict.items():
ue = db.session.get(UniteEns, ue_id) ue = db.session.get(UniteEns, ue_id)
@ -474,7 +474,7 @@ class EvaluationUEPoids(db.Model):
backref=db.backref("ue_poids", cascade="all, delete-orphan"), backref=db.backref("ue_poids", cascade="all, delete-orphan"),
) )
ue = db.relationship( ue = db.relationship(
UniteEns, "UniteEns",
backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"), backref=db.backref("evaluation_ue_poids", cascade="all, delete-orphan"),
) )
@ -506,7 +506,7 @@ def evaluation_enrich_dict(e: Evaluation, e_dict: dict):
return e_dict return e_dict
def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict): def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
"""Check coefficient, dates and duration, raises exception if invalid. """Check coefficient, dates and duration, raises exception if invalid.
Convert date and time strings to date and time objects. Convert date and time strings to date and time objects.
@ -606,19 +606,6 @@ def heure_to_time(heure: str) -> datetime.time:
return datetime.time(int(h), int(m)) return datetime.time(int(h), int(m))
def _time_duration_HhM(heure_debut: str, heure_fin: str) -> int:
"""duree (nb entier de minutes) entre deux heures a notre format
ie 12h23
"""
if heure_debut and heure_fin:
h0, m0 = [int(x) for x in heure_debut.split("h")]
h1, m1 = [int(x) for x in heure_fin.split("h")]
d = (h1 - h0) * 60 + (m1 - m0)
return d
else:
return None
def _moduleimpl_evaluation_insert_before( def _moduleimpl_evaluation_insert_before(
evaluations: list[Evaluation], next_eval: Evaluation evaluations: list[Evaluation], next_eval: Evaluation
) -> int: ) -> int:

View File

@ -13,7 +13,6 @@ from app import email
from app import log from app import log
from app.auth.models import User from app.auth.models import User
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -181,6 +180,7 @@ class ScolarNews(db.Model):
None si inexistant None si inexistant
""" """
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
formsemestre_id = None formsemestre_id = None
if self.type == self.NEWS_INSCR: if self.type == self.NEWS_INSCR:

View File

@ -390,7 +390,7 @@ class FormSemestre(db.Model):
Module.numero, Module.numero,
Module.code, Module.code,
Evaluation.numero, Evaluation.numero,
Evaluation.date_debut.desc(), Evaluation.date_debut,
) )
.all() .all()
) )

View File

@ -3,12 +3,14 @@
""" """
import pandas as pd import pandas as pd
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
import sqlalchemy as sa
from app import db from app import db
from app.auth.models import User from app.auth.models import User
from app.comp import df_cache from app.comp import df_cache
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.modules import Module from app.models.modules import Module
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -38,7 +40,13 @@ class ModuleImpl(db.Model):
# formule de calcul moyenne: # formule de calcul moyenne:
computation_expr = db.Column(db.Text()) computation_expr = db.Column(db.Text())
evaluations = db.relationship("Evaluation", lazy="dynamic", backref="moduleimpl") evaluations = db.relationship(
"Evaluation",
lazy="dynamic",
backref="moduleimpl",
order_by=(Evaluation.numero, Evaluation.date_debut),
)
"évaluations, triées par numéro et dates croissants, donc la plus ancienne d'abord."
enseignants = db.relationship( enseignants = db.relationship(
"User", "User",
secondary="notes_modules_enseignants", secondary="notes_modules_enseignants",

View File

@ -28,6 +28,7 @@
"""Vérification des absences à une évaluation """Vérification des absences à une évaluation
""" """
from flask import url_for, g from flask import url_for, g
from flask_sqlalchemy.query import Query
from app import db from app import db
from app.models import Evaluation, FormSemestre, Identite, Assiduite from app.models import Evaluation, FormSemestre, Identite, Assiduite
@ -37,9 +38,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_groups from app.scodoc import sco_groups
from flask_sqlalchemy.query import Query
from sqlalchemy import or_, and_
def evaluation_check_absences(evaluation: Evaluation): def evaluation_check_absences(evaluation: Evaluation):
"""Vérifie les absences au moment de cette évaluation. """Vérifie les absences au moment de cette évaluation.
@ -78,11 +76,11 @@ def evaluation_check_absences(evaluation: Evaluation):
# Les notes: # Les notes:
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
ValButAbs = [] # une note mais noté absent note_but_abs = [] # une note mais noté absent
AbsNonSignalee = [] # note ABS mais pas noté absent abs_non_signalee = [] # note ABS mais pas noté absent
ExcNonSignalee = [] # note EXC mais pas noté absent exc_non_signalee = [] # note EXC mais pas noté absent
ExcNonJust = [] # note EXC mais absent non justifie exc_non_just = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié abs_but_exc = [] # note ABS mais justifié
for etudid in etudids: for etudid in etudids:
if etudid in notes_db: if etudid in notes_db:
val = notes_db[etudid]["value"] val = notes_db[etudid]["value"]
@ -92,50 +90,43 @@ def evaluation_check_absences(evaluation: Evaluation):
and val != scu.NOTES_ATTENTE and val != scu.NOTES_ATTENTE
) and etudid in abs_etudids: ) and etudid in abs_etudids:
# note valide et absent # note valide et absent
ValButAbs.append(etudid) note_but_abs.append(etudid)
if val is None and not etudid in abs_etudids: if val is None and not etudid in abs_etudids:
# absent mais pas signale comme tel # absent mais pas signale comme tel
AbsNonSignalee.append(etudid) abs_non_signalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and not etudid in abs_etudids: if val == scu.NOTES_NEUTRALISE and not etudid in abs_etudids:
# Neutralisé mais pas signale absent # Neutralisé mais pas signale absent
ExcNonSignalee.append(etudid) exc_non_signalee.append(etudid)
if val == scu.NOTES_NEUTRALISE and etudid in abs_nj_etudids: if val == scu.NOTES_NEUTRALISE and etudid in abs_nj_etudids:
# EXC mais pas justifié # EXC mais pas justifié
ExcNonJust.append(etudid) exc_non_just.append(etudid)
if val is None and etudid in just_etudids: if val is None and etudid in just_etudids:
# ABS mais justificatif # ABS mais justificatif
AbsButExc.append(etudid) abs_but_exc.append(etudid)
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc return note_but_abs, abs_non_signalee, exc_non_signalee, exc_non_just, abs_but_exc
def evaluation_check_absences_html( def evaluation_check_absences_html(
evaluation: Evaluation, with_header=True, show_ok=True evaluation: Evaluation, with_header=True, show_ok=True
): ):
"""Affiche état vérification absences d'une évaluation""" """Affiche état vérification absences d'une évaluation"""
am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
# 1 si matin, 0 si apres midi, 2 si toute la journee:
match am, pm:
case False, True:
demijournee = 0
case True, False:
demijournee = 1
case _:
demijournee = 2
( (
ValButAbs, note_but_abs, # une note alors qu'il était signalé abs
AbsNonSignalee, abs_non_signalee, # note ABS alors que pas signalé abs
ExcNonSignalee, exc_non_signalee, # note EXC alors que pas signalé abs
ExcNonJust, exc_non_just, # note EXC alors que pas de justif
AbsButExc, abs_but_exc, # note ABS alors qu'il y a un justif
) = evaluation_check_absences(evaluation) ) = evaluation_check_absences(evaluation)
if with_header: if with_header:
H = [ H = [
html_sco_header.html_sem_header("Vérification absences à l'évaluation"), html_sco_header.html_sem_header(
"Vérification absences à l'évaluation",
formsemestre_id=evaluation.moduleimpl.formsemestre_id,
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation.id), sco_evaluations.evaluation_describe(evaluation_id=evaluation.id),
"""<p class="help">Vérification de la cohérence entre les notes saisies """<p class="help">Vérification de la cohérence entre les notes saisies
et les absences signalées.</p>""", et les absences signalées.</p>""",
] ]
else: else:
@ -148,10 +139,10 @@ def evaluation_check_absences_html(
} """ } """
] ]
if ( if (
not ValButAbs not note_but_abs
and not AbsNonSignalee and not abs_non_signalee
and not ExcNonSignalee and not exc_non_signalee
and not ExcNonJust and not exc_non_just
): ):
H.append(': <span class="eval_check_absences_ok">ok</span>') H.append(': <span class="eval_check_absences_ok">ok</span>')
H.append("</h2>") H.append("</h2>")
@ -171,46 +162,50 @@ def evaluation_check_absences_html(
) )
if linkabs: if linkabs:
url = url_for( url = url_for(
"assiduites.signal_evaluation_abs", "assiduites.signale_evaluation_abs",
etudid=etudid, etudid=etudid,
evaluation_id=evaluation.id, evaluation_id=evaluation.id,
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
) )
H.append( H.append(
f"""<a class="stdlink" href="{url}">signaler cette absence</a>""" f"""<a style="margin-left: 16px;" class="stdlink" href="{
url}">signaler cette absence</a>"""
) )
H.append("</li>") H.append("</li>")
H.append("</ul>") H.append("</ul>")
if ValButAbs or show_ok: if note_but_abs or show_ok:
H.append( H.append(
"<h3>Etudiants ayant une note alors qu'ils sont signalés absents:</h3>" "<h3>Étudiants ayant une note alors qu'ils sont signalés absents:</h3>"
) )
etudlist(ValButAbs) etudlist(note_but_abs)
if AbsNonSignalee or show_ok: if abs_non_signalee or show_ok:
H.append( H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>""" """<h3>Étudiants avec note "ABS" alors qu'ils ne sont
<em>pas</em> signalés absents:</h3>"""
) )
etudlist(AbsNonSignalee, linkabs=True) etudlist(abs_non_signalee, linkabs=True)
if ExcNonSignalee or show_ok: if exc_non_signalee or show_ok:
H.append( H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils ne sont <em>pas</em> signalés absents:</h3>""" """<h3>Étudiants avec note "EXC" alors qu'ils ne sont
<em>pas</em> signalés absents:</h3>"""
) )
etudlist(ExcNonSignalee) etudlist(exc_non_signalee)
if ExcNonJust or show_ok: if exc_non_just or show_ok:
H.append( H.append(
"""<h3>Etudiants avec note "EXC" alors qu'ils sont absents <em>non justifiés</em>:</h3>""" """<h3>Étudiants avec note "EXC" alors qu'ils sont absents
<em>non justifiés</em>:</h3>"""
) )
etudlist(ExcNonJust) etudlist(exc_non_just)
if AbsButExc or show_ok: if abs_but_exc or show_ok:
H.append( H.append(
"""<h3>Etudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>""" """<h3>Étudiants avec note "ABS" alors qu'ils ont une <em>justification</em>:</h3>"""
) )
etudlist(AbsButExc) etudlist(abs_but_exc)
if with_header: if with_header:
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
@ -226,7 +221,8 @@ def formsemestre_check_absences_html(formsemestre_id):
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre", "Vérification absences aux évaluations de ce semestre",
), ),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées. """<p class="help">Vérification de la cohérence entre les notes saisies
et les absences signalées.
Sont listés tous les modules avec des évaluations.<br>Aucune action n'est effectuée: Sont listés tous les modules avec des évaluations.<br>Aucune action n'est effectuée:
il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire. il vous appartient de corriger les erreurs détectées si vous le jugez nécessaire.
</p>""", </p>""",
@ -237,14 +233,12 @@ def formsemestre_check_absences_html(formsemestre_id):
H.append( H.append(
f"""<div class="module_check_absences"> f"""<div class="module_check_absences">
<h2><a href="{ <h2><a href="{
url_for("notes.moduleimpl_status", url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id) scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">{modimpl.module.code or ""}: {modimpl.module.abbrev or ""}</a> }">{modimpl.module.code or ""}: {modimpl.module.abbrev or ""}</a>
</h2>""" </h2>"""
) )
for evaluation in modimpl.evaluations.order_by( for evaluation in modimpl.evaluations:
Evaluation.numero, Evaluation.date_debut
):
H.append( H.append(
evaluation_check_absences_html( evaluation_check_absences_html(
evaluation, evaluation,

View File

@ -33,8 +33,8 @@ import operator
from flask import url_for from flask import url_for
from flask import g from flask import g
from flask_login import current_user
from flask import request from flask import request
from flask_login import current_user
from app import db from app import db
from app.auth.models import User from app.auth.models import User
@ -50,11 +50,9 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_cal from app.scodoc import sco_cal
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
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
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
import sco_version import sco_version
@ -76,22 +74,21 @@ def notes_moyenne_median_mini_maxi(notes):
if not n: if not n:
return None, None, None, None return None, None, None, None
moy = sum(notes) / n moy = sum(notes) / n
median = ListMedian(notes) median = list_median(notes)
mini = min(notes) mini = min(notes)
maxi = max(notes) maxi = max(notes)
return moy, median, mini, maxi return moy, median, mini, maxi
def ListMedian(L): def list_median(a_list: list):
"""Median of a list L""" """Median of a list L"""
n = len(L) n = len(a_list)
if not n: if not n:
raise ValueError("empty list") raise ValueError("empty list")
L.sort() a_list.sort()
if n % 2: if n % 2:
return L[n // 2] return a_list[n // 2]
else: return (a_list[n // 2] + a_list[n // 2 - 1]) / 2
return (L[n // 2] + L[n // 2 - 1]) / 2
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -190,39 +187,39 @@ def do_evaluation_etat(
# On considere une note "manquante" lorsqu'elle n'existe pas # On considere une note "manquante" lorsqu'elle n'existe pas
# ou qu'elle est en attente (ATT) # ou qu'elle est en attente (ATT)
GrNbMissing = collections.defaultdict(int) # group_id : nb notes manquantes group_nb_missing = collections.defaultdict(int) # group_id : nb notes manquantes
GrNotes = collections.defaultdict(list) # group_id: liste notes valides group_notes = collections.defaultdict(list) # group_id: liste notes valides
TotalNbMissing = 0 total_nb_missing = 0
TotalNbAtt = 0 total_nb_att = 0
groups = {} # group_id : group group_by_id = {} # group_id : group
etud_groups = sco_groups.get_etud_groups_in_partition(partition_id) etud_groups = sco_groups.get_etud_groups_in_partition(partition_id)
for i in ins: for i in ins:
group = etud_groups.get(i["etudid"], None) group = etud_groups.get(i["etudid"], None)
if group and not group["group_id"] in groups: if group and not group["group_id"] in group_by_id:
groups[group["group_id"]] = group group_by_id[group["group_id"]] = group
# #
isMissing = False is_missing = False
if i["etudid"] in etuds_notes_dict: if i["etudid"] in etuds_notes_dict:
val = etuds_notes_dict[i["etudid"]]["value"] val = etuds_notes_dict[i["etudid"]]["value"]
if val == scu.NOTES_ATTENTE: if val == scu.NOTES_ATTENTE:
isMissing = True is_missing = True
TotalNbAtt += 1 total_nb_att += 1
if group: if group:
GrNotes[group["group_id"]].append(val) group_notes[group["group_id"]].append(val)
else: else:
if group: if group:
_ = GrNotes[group["group_id"]] # create group _ = group_notes[group["group_id"]] # create group
isMissing = True is_missing = True
if isMissing: if is_missing:
TotalNbMissing += 1 total_nb_missing += 1
if group: if group:
GrNbMissing[group["group_id"]] += 1 group_nb_missing[group["group_id"]] += 1
gr_incomplets = [x for x in GrNbMissing.keys()] gr_incomplets = list(group_nb_missing.keys())
gr_incomplets.sort() gr_incomplets.sort()
if ( if (
(TotalNbMissing > 0) (total_nb_missing > 0)
and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE) and (E["evaluation_type"] != scu.EVALUATION_RATTRAPAGE)
and (E["evaluation_type"] != scu.EVALUATION_SESSION2) and (E["evaluation_type"] != scu.EVALUATION_SESSION2)
): ):
@ -231,12 +228,12 @@ def do_evaluation_etat(
complete = True complete = True
complete = ( complete = (
(TotalNbMissing == 0) (total_nb_missing == 0)
or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE) or (E["evaluation_type"] == scu.EVALUATION_RATTRAPAGE)
or (E["evaluation_type"] == scu.EVALUATION_SESSION2) or (E["evaluation_type"] == scu.EVALUATION_SESSION2)
) )
evalattente = (TotalNbMissing > 0) and ( evalattente = (total_nb_missing > 0) and (
(TotalNbMissing == TotalNbAtt) or E["publish_incomplete"] (total_nb_missing == total_nb_att) or E["publish_incomplete"]
) )
# mais ne met pas en attente les evals immediates sans aucune notes: # mais ne met pas en attente les evals immediates sans aucune notes:
if E["publish_incomplete"] and nb_notes == 0: if E["publish_incomplete"] and nb_notes == 0:
@ -244,12 +241,12 @@ def do_evaluation_etat(
# Calcul moyenne dans chaque groupe de TD # Calcul moyenne dans chaque groupe de TD
gr_moyennes = [] # group : {moy,median, nb_notes} gr_moyennes = [] # group : {moy,median, nb_notes}
for group_id, notes in GrNotes.items(): for group_id, notes in group_notes.items():
gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes) gr_moy, gr_median, gr_mini, gr_maxi = notes_moyenne_median_mini_maxi(notes)
gr_moyennes.append( gr_moyennes.append(
{ {
"group_id": group_id, "group_id": group_id,
"group_name": groups[group_id]["group_name"], "group_name": group_by_id[group_id]["group_name"],
"gr_moy": scu.fmt_note(gr_moy, E["note_max"]), "gr_moy": scu.fmt_note(gr_moy, E["note_max"]),
"gr_median": scu.fmt_note(gr_median, E["note_max"]), "gr_median": scu.fmt_note(gr_median, E["note_max"]),
"gr_mini": scu.fmt_note(gr_mini, E["note_max"]), "gr_mini": scu.fmt_note(gr_mini, E["note_max"]),
@ -276,7 +273,7 @@ def do_evaluation_etat(
"last_modif": last_modif, "last_modif": last_modif,
"gr_incomplets": gr_incomplets, "gr_incomplets": gr_incomplets,
"gr_moyennes": gr_moyennes, "gr_moyennes": gr_moyennes,
"groups": groups, "groups": group_by_id,
"evalcomplete": complete, "evalcomplete": complete,
"evalattente": evalattente, "evalattente": evalattente,
"is_malus": is_malus, "is_malus": is_malus,
@ -413,7 +410,7 @@ def do_evaluation_etat_in_sem(formsemestre_id):
def do_evaluation_etat_in_mod(nt, moduleimpl_id): def do_evaluation_etat_in_mod(nt, moduleimpl_id):
"""""" """état des évaluations dans ce module"""
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id) evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
etat = _eval_etat(evals) etat = _eval_etat(evals)
# Il y a-t-il des notes en attente dans ce module ? # Il y a-t-il des notes en attente dans ce module ?
@ -426,7 +423,7 @@ def formsemestre_evaluations_cal(formsemestre_id):
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
evaluations = formsemestre.get_evaluations() # TODO evaluations = formsemestre.get_evaluations()
nb_evals = len(evaluations) nb_evals = len(evaluations)
color_incomplete = "#FF6060" color_incomplete = "#FF6060"
@ -642,7 +639,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
# -------------- VIEWS # -------------- VIEWS
def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True): def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True) -> str:
"""HTML description of evaluation, for page headers """HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented) edit_in_place: allow in-place editing when permitted (not implemented)
""" """
@ -696,7 +693,15 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
date_debut=evaluation.date_debut.isoformat(), date_debut=evaluation.date_debut.isoformat(),
date_fin=evaluation.date_fin.isoformat(), date_fin=evaluation.date_fin.isoformat(),
) )
}">absences ce jour</a></span>""" }">absences ce jour</a>
</span>
<span class="evallink"><a class="stdlink" href="{url_for(
'notes.evaluation_check_absences_html',
scodoc_dept=g.scodoc_dept,
evaluation_id = evaluation.id)
}">vérifier notes vs absences</a>
</span>
"""
) )
else: else:
H.append("<p><em>sans date</em> ") H.append("<p><em>sans date</em> ")

View File

@ -33,18 +33,15 @@ import numpy as np
import flask import flask
from flask import url_for, g, request from flask import url_for, g, request
from app import db, log
from app import models
from app.comp import res_sem from app.comp import res_sem
from app.comp import moy_mod from app.comp import moy_mod
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Module from app.models import FormSemestre, Module
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_etud import etud_sort_key from app.scodoc.sco_etud import etud_sort_key
@ -54,58 +51,58 @@ from app.scodoc import sco_groups
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.htmlutils import histogram_notes from app.scodoc.htmlutils import histogram_notes
import sco_version
def do_evaluation_listenotes( def do_evaluation_listenotes(
evaluation_id=None, moduleimpl_id=None, fmt="html" evaluation_id=None, moduleimpl_id=None, fmt="html"
) -> tuple[str, str]: ) -> tuple[str | flask.Response, str]:
""" """
Affichage des notes d'une évaluation (si evaluation_id) Affichage des notes d'une évaluation (si evaluation_id)
ou de toutes les évaluations d'un module (si moduleimpl_id) ou de toutes les évaluations d'un module (si moduleimpl_id)
""" """
mode = None mode = None
if moduleimpl_id: evaluations: list[Evaluation] = []
if moduleimpl_id is not None:
mode = "module" mode = "module"
evals = sco_evaluation_db.get_evaluations_dict({"moduleimpl_id": moduleimpl_id}) modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
elif evaluation_id: evaluations = modimpl.evaluations.all()
elif evaluation_id is not None:
mode = "eval" mode = "eval"
evals = sco_evaluation_db.get_evaluations_dict({"evaluation_id": evaluation_id}) evaluations = Evaluation.query.filter_by(id=evaluation_id).all()
else: else:
raise ValueError("missing argument: evaluation or module") raise ValueError("missing argument: evaluation or module")
if not evals: if not evaluations:
return "<p>Aucune évaluation !</p>", "ScoDoc" return "<p>Aucune évaluation !</p>", "ScoDoc"
evaluation = evaluations[0]
modimpl = evaluation.moduleimpl # il y a au moins une evaluation
eval_dict = evals[0] # il y a au moins une evaluation
modimpl = db.session.get(ModuleImpl, eval_dict["moduleimpl_id"])
# description de l'evaluation # description de l'evaluation
if mode == "eval": if evaluation_id is not None:
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)] H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
page_title = f"Notes {eval_dict['description'] or modimpl.module.code}" page_title = f"Notes {evaluation.description or modimpl.module.code}"
else: else:
H = [] H = []
page_title = f"Notes {modimpl.module.code}" page_title = f"Notes {modimpl.module.code}"
# groupes # groupes
groups = sco_groups.do_evaluation_listegroupes( groups = sco_groups.do_evaluation_listegroupes(evaluation.id, include_default=True)
eval_dict["evaluation_id"], include_default=True
)
grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons 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 grnams = [str(g["group_id"]) for g in groups] # noms des checkbox
if len(evals) > 1: if len(evaluations) > 1:
descr = [ descr = [
( (
"moduleimpl_id", "moduleimpl_id",
{"default": eval_dict["moduleimpl_id"], "input_type": "hidden"}, {"default": modimpl.id, "input_type": "hidden"},
) )
] ]
else: else:
descr = [ descr = [
( (
"evaluation_id", "evaluation_id",
{"default": eval_dict["evaluation_id"], "input_type": "hidden"}, {"default": evaluation.id, "input_type": "hidden"},
) )
] ]
if len(grnams) > 1: if len(grnams) > 1:
@ -148,7 +145,8 @@ def do_evaluation_listenotes(
"allowed_values": ("yes",), "allowed_values": ("yes",),
"labels": ('listing "anonyme"',), "labels": ('listing "anonyme"',),
"attributes": ('onclick="document.tf.submit();"',), "attributes": ('onclick="document.tf.submit();"',),
"template": '<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s &nbsp;&nbsp;', "template": """<tr><td class="tf-fieldlabel">%(label)s</td>
<td class="tf-field">%(elem)s &nbsp;&nbsp;""",
}, },
), ),
( (
@ -205,7 +203,7 @@ def do_evaluation_listenotes(
url_for( url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=eval_dict["moduleimpl_id"], moduleimpl_id=modimpl.id,
) )
), ),
"", "",
@ -219,7 +217,7 @@ def do_evaluation_listenotes(
return ( return (
_make_table_notes( _make_table_notes(
tf[1], tf[1],
evals, evaluations,
fmt=fmt, fmt=fmt,
note_sur_20=note_sur_20, note_sur_20=note_sur_20,
anonymous_listing=anonymous_listing, anonymous_listing=anonymous_listing,
@ -234,37 +232,36 @@ def do_evaluation_listenotes(
def _make_table_notes( def _make_table_notes(
html_form, html_form,
evals, evaluations: list[Evaluation],
fmt: str = "", fmt: str = "",
note_sur_20=False, note_sur_20=False,
anonymous_listing=False, anonymous_listing=False,
hide_groups=False, hide_groups=False,
with_emails=False, with_emails=False,
group_ids: list[int] = None, group_ids: list[int] | None = None,
mode="module", # "eval" or "module" mode="module", # "eval" or "module"
) -> str: ) -> str:
"""Table liste notes (une seule évaluation ou toutes celles d'un module)""" """Table liste notes (une seule évaluation ou toutes celles d'un module)"""
group_ids = group_ids or [] group_ids = group_ids or []
if not evals: if not evaluations:
return "<p>Aucune évaluation !</p>" return "<p>Aucune évaluation !</p>"
E = evals[0] evaluation = evaluations[0]
moduleimpl_id = E["moduleimpl_id"] modimpl = evaluation.moduleimpl
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
module: Module = modimpl.module module: Module = modimpl.module
formsemestre: FormSemestre = modimpl.formsemestre formsemestre: FormSemestre = modimpl.formsemestre
is_apc = module.formation.get_cursus().APC_SAE is_apc = module.formation.is_apc()
if is_apc: if is_apc:
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
is_conforme = modimpl.check_apc_conformity(res) is_conforme = modimpl.check_apc_conformity(res)
evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id) evals_poids, ues = moy_mod.load_evaluations_poids(modimpl.id)
if not ues: if not ues:
is_apc = False is_apc = False
else: else:
evals_poids, ues = None, None evals_poids, ues = None, None
is_conforme = True is_conforme = True
# (debug) check that all evals are in same module: # (debug) check that all evals are in same module:
for e in evals: for e in evaluations:
if e["moduleimpl_id"] != moduleimpl_id: if e.moduleimpl_id != modimpl.id:
raise ValueError("invalid evaluations list") raise ValueError("invalid evaluations list")
if fmt == "xls": if fmt == "xls":
@ -302,11 +299,14 @@ def _make_table_notes(
} }
rows = [] rows = []
class KeyManager(dict): # comment : key (pour regrouper les comments a la fin) class KeyManager(dict):
"comment : key (pour regrouper les comments a la fin)"
def __init__(self): def __init__(self):
self.lastkey = 1 self.lastkey = 1
def nextkey(self): def nextkey(self) -> str:
"get new key (int)"
r = self.lastkey r = self.lastkey
self.lastkey += 1 self.lastkey += 1
# self.lastkey = chr(ord(self.lastkey)+1) # self.lastkey = chr(ord(self.lastkey)+1)
@ -323,7 +323,7 @@ def _make_table_notes(
anonymous_lst_key = "etudid" anonymous_lst_key = "etudid"
etudid_etats = sco_groups.do_evaluation_listeetuds_groups( etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
E["evaluation_id"], groups, include_demdef=True evaluation.id, groups, include_demdef=True
) )
for etudid, etat in etudid_etats: for etudid, etat in etudid_etats:
css_row_class = None css_row_class = None
@ -360,7 +360,8 @@ def _make_table_notes(
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
etudid=etudid, etudid=etudid,
), ),
"_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{etud.sort_key}" """, "_nomprenom_td_attrs": f"""id="{etudid}" class="etudinfo" data-sort="{
etud.sort_key}" """,
"prenom": etud.prenom.lower().capitalize(), "prenom": etud.prenom.lower().capitalize(),
"nom_usuel": etud.nom_usuel, "nom_usuel": etud.nom_usuel,
"nomprenom": etud.nomprenom, "nomprenom": etud.nomprenom,
@ -408,10 +409,12 @@ def _make_table_notes(
"comment": "", "comment": "",
} }
# Ajoute les notes de chaque évaluation: # Ajoute les notes de chaque évaluation:
for e in evals: evals_state: dict[int, dict] = {}
e["eval_state"] = sco_evaluations.do_evaluation_etat(e["evaluation_id"]) for e in evaluations:
evals_state[e.id] = sco_evaluations.do_evaluation_etat(e.id)
notes, nb_abs, nb_att = _add_eval_columns( notes, nb_abs, nb_att = _add_eval_columns(
e, e,
evals_state[e.id],
evals_poids, evals_poids,
ues, ues,
rows, rows,
@ -426,7 +429,7 @@ def _make_table_notes(
keep_numeric, keep_numeric,
fmt=fmt, fmt=fmt,
) )
columns_ids.append(e["evaluation_id"]) columns_ids.append(e.id)
# #
if anonymous_listing: if anonymous_listing:
rows.sort(key=lambda x: x["code"] or "") rows.sort(key=lambda x: x["code"] or "")
@ -436,12 +439,12 @@ def _make_table_notes(
# Si module, ajoute la (les) "moyenne(s) du module: # Si module, ajoute la (les) "moyenne(s) du module:
if mode == "module": if mode == "module":
if len(evals) > 1: if len(evaluations) > 1:
# Moyenne de l'étudiant dans le module # Moyenne de l'étudiant dans le module
# Affichée même en APC à titre indicatif # Affichée même en APC à titre indicatif
_add_moymod_column( _add_moymod_column(
formsemestre.id, formsemestre.id,
moduleimpl_id, modimpl.id,
rows, rows,
columns_ids, columns_ids,
titles, titles,
@ -473,7 +476,7 @@ def _make_table_notes(
if with_emails: if with_emails:
columns_ids += ["email", "emailperso"] columns_ids += ["email", "emailperso"]
# Ajoute lignes en tête et moyennes # Ajoute lignes en tête et moyennes
if len(evals) > 0 and fmt != "bordereau": if len(evaluations) > 0 and fmt != "bordereau":
rows_head = [row_coefs] rows_head = [row_coefs]
if is_apc: if is_apc:
rows_head.append(row_poids) rows_head.append(row_poids)
@ -481,22 +484,22 @@ def _make_table_notes(
rows = rows_head + rows rows = rows_head + rows
rows.append(row_moys) rows.append(row_moys)
# ajout liens HTMl vers affichage une evaluation: # ajout liens HTMl vers affichage une evaluation:
if fmt == "html" and len(evals) > 1: if fmt == "html" and len(evaluations) > 1:
rlinks = {"_table_part": "head"} rlinks = {"_table_part": "head"}
for e in evals: for e in evaluations:
rlinks[e["evaluation_id"]] = "afficher" rlinks[e.id] = "afficher"
rlinks[ rlinks[
"_" + str(e["evaluation_id"]) + "_help" "_" + str(e.id) + "_help"
] = "afficher seulement les notes de cette évaluation" ] = "afficher seulement les notes de cette évaluation"
rlinks["_" + str(e["evaluation_id"]) + "_target"] = url_for( rlinks["_" + str(e.id) + "_target"] = url_for(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
evaluation_id=e["evaluation_id"], evaluation_id=e.id,
) )
rlinks["_" + str(e["evaluation_id"]) + "_td_attrs"] = ' class="tdlink" ' rlinks["_" + str(e.id) + "_td_attrs"] = ' class="tdlink" '
rows.append(rlinks) rows.append(rlinks)
if len(evals) == 1: # colonne "Rem." seulement si une eval if len(evaluations) == 1: # colonne "Rem." seulement si une eval
if fmt == "html": # pas d'indication d'origine en pdf (pour affichage) if fmt == "html": # pas d'indication d'origine en pdf (pour affichage)
columns_ids.append("expl_key") columns_ids.append("expl_key")
elif fmt == "xls" or fmt == "xml": elif fmt == "xls" or fmt == "xml":
@ -514,68 +517,84 @@ def _make_table_notes(
gl = "&hide_groups%3Alist=yes" + gl gl = "&hide_groups%3Alist=yes" + gl
if with_emails: if with_emails:
gl = "&with_emails%3Alist=yes" + gl gl = "&with_emails%3Alist=yes" + gl
if len(evals) == 1: if len(evaluations) == 1:
evalname = "%s-%s" % (module.code, ndb.DateDMYtoISO(E["jour"])) evalname = f"""{module.code}-{
hh = "%s, %s (%d étudiants)" % (E["description"], gr_title, len(etudid_etats)) evaluation.date_debut.replace(tzinfo=None).isoformat()
filename = scu.make_filename("notes_%s_%s" % (evalname, gr_title_filename)) 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": if fmt == "bordereau":
hh = " %d étudiants" % (len(etudid_etats)) hh = f""" {len(etudid_etats)} étudiants {
hh += " %d absent" % (nb_abs) nb_abs} absent{'s' if nb_abs > 1 else ''}, {nb_att} en attente."""
if nb_abs > 1:
hh += "s"
hh += ", %d en attente." % (nb_att)
# Attention: ReportLab supporte seulement '<br/>', pas '<br>' ! # Attention: ReportLab supporte seulement '<br/>', pas '<br>' !
pdf_title = f"""<br/> BORDEREAU DE SIGNATURES pdf_title = f"""<br/> BORDEREAU DE SIGNATURES
<br/><br/>{formsemestre.titre or ''} <br/><br/>{formsemestre.titre or ''}
<br/>({formsemestre.mois_debut()} - {formsemestre.mois_fin()}) <br/>({formsemestre.mois_debut()} - {formsemestre.mois_fin()})
semestre {formsemestre.semestre_id} {formsemestre.modalite or ""} semestre {formsemestre.semestre_id} {formsemestre.modalite or ""}
<br/>Notes du module {module.code} - {module.titre} <br/>Notes du module {module.code} - {module.titre}
<br/>Évaluation : {e["description"]} <br/>Évaluation : {evaluation.description}
""" """
if len(e["jour"]) > 0: if evaluation.date_debut:
pdf_title += " (%(jour)s)" % e pdf_title += f" ({evaluation.date_debut.strftime('%d/%m/%Y')})"
pdf_title += "(noté sur %(note_max)s )<br/><br/>" % e pdf_title += "(noté sur {evaluation.note_max} )<br/><br/>"
else: else:
hh = " %s, %s (%d étudiants)" % ( hh = " %s, %s (%d étudiants)" % (
E["description"], evaluation.description,
gr_title, gr_title,
len(etudid_etats), len(etudid_etats),
) )
if len(e["jour"]) > 0: if evaluation.date_debut:
pdf_title = "%(description)s (%(jour)s)" % e pdf_title = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
else: else:
pdf_title = "%(description)s " % e pdf_title = evaluation.description or f"évaluation dans {module.code}"
caption = hh caption = hh
html_title = "" html_title = ""
base_url = "evaluation_listenotes?evaluation_id=%s" % E["evaluation_id"] + gl base_url = (
html_next_section = ( url_for(
'<div class="notes_evaluation_stats">%d absents, %d en attente.</div>' "notes.evaluation_listenotes",
% (nb_abs, nb_att) 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: else:
# Plusieurs évaluations (module)
filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename)) filename = scu.make_filename("notes_%s_%s" % (module.code, gr_title_filename))
title = f"Notes {module.type_name()} {module.code} {module.titre}" title = f"Notes {module.type_name()} {module.code} {module.titre}"
title += f""" semestre {formsemestre.titre_mois()}""" title += f""" semestre {formsemestre.titre_mois()}"""
if gr_title and gr_title != "tous": if gr_title and gr_title != "tous":
title += " %s" % gr_title title += " {gr_title}"
caption = title caption = title
html_next_section = "" html_next_section = ""
if fmt == "pdf" or fmt == "bordereau": if fmt == "pdf" or fmt == "bordereau":
caption = "" # same as pdf_title caption = "" # same as pdf_title
pdf_title = title pdf_title = title
html_title = f"""<h2 class="formsemestre">Notes {module.type_name()} <a href="{ html_title = f"""<h2 class="formsemestre">Notes {module.type_name()}
url_for("notes.moduleimpl_status", <a class="stdlink" href="{
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id) url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">{module.code} {module.titre}</a></h2> }">{module.code} {module.titre}</a></h2>
""" """
if not is_conforme: if not is_conforme:
html_title += ( html_title += (
"""<div class="warning">Poids des évaluations non conformes !</div>""" """<div class="warning">Poids des évaluations non conformes !</div>"""
) )
base_url = "evaluation_listenotes?moduleimpl_id=%s" % moduleimpl_id + gl base_url = (
url_for(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl.id,
)
+ gl
)
# display # display
tab = GenTable( tab = GenTable(
titles=titles, titles=titles,
@ -600,64 +619,70 @@ def _make_table_notes(
if fmt != "html": if fmt != "html":
return t return t
if len(evals) > 1: if len(evaluations) > 1:
all_complete = True all_complete = True
for e in evals: for e in evaluations:
if not e["eval_state"]["evalcomplete"]: if not evals_state[e.id]["evalcomplete"]:
all_complete = False all_complete = False
if all_complete: if all_complete:
eval_info = """<span class="eval_info"><span class="eval_complete">Évaluations eval_info = """<span class="eval_info"><span class="eval_complete">Évaluations
prises en compte dans les moyennes.</span>""" prises en compte dans les moyennes.</span>"""
else: else:
eval_info = """<span class="eval_info help"> eval_info = """<span class="eval_info help">
Les évaluations en vert et orange sont prises en compte dans les moyennes. Les évaluations en vert et orange sont prises en compte dans les moyennes.
Celles en rouge n'ont pas toutes leurs notes.""" Celles en rouge n'ont pas toutes leurs notes."""
if is_apc: if is_apc:
eval_info += """ <span>La moyenne indicative est la moyenne des moyennes d'UE, et n'est pas utilisée en BUT. eval_info += """ <span>La moyenne indicative est la moyenne des moyennes d'UE,
Les moyennes sur le groupe sont estimées sans les absents (sauf pour les moyennes des moyennes d'UE) ni les démissionnaires.</span>""" 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>""" eval_info += """</span>"""
return html_form + eval_info + t + "<p></p>" return html_form + eval_info + t + "<p></p>"
else: # Une seule evaluation: ajoute histogramme
# Une seule evaluation: ajoute histogramme histo = histogram_notes(notes)
histo = histogram_notes(notes) # 2 colonnes: histo, comments
# 2 colonnes: histo, comments C = [
C = [ f"""<br><a class="stdlink" href="{base_url}&fmt=bordereau">Bordereau de Signatures (version PDF)</a>
f'<br><a class="stdlink" href="{base_url}&fmt=bordereau">Bordereau de Signatures (version PDF)</a>', <table>
"<table><tr><td><div><h4>Répartition des notes:</h4>" <tr><td>
+ histo <div><h4>Répartition des notes:</h4>
+ "</div></td>\n", {histo}
'<td style="padding-left: 50px; vertical-align: top;"><p>', </div>
] </td>
commentkeys = list(key_mgr.items()) # [ (comment, key), ... ] <td style="padding-left: 50px; vertical-align: top;"><p>
commentkeys.sort(key=lambda x: int(x[1])) """
for comment, key in commentkeys: ]
C.append( commentkeys = list(key_mgr.items()) # [ (comment, key), ... ]
'<span class="colcomment">(%s)</span> <em>%s</em><br>' % (key, comment) commentkeys.sort(key=lambda x: int(x[1]))
) for comment, key in commentkeys:
if commentkeys: C.append(f"""<span class="colcomment">({key})</span> <em>{comment}</em><br>""")
C.append( if commentkeys:
'<span><a class=stdlink" href="evaluation_list_operations?evaluation_id=%s">Gérer les opérations</a></span><br>' C.append(
% E["evaluation_id"] f"""<span><a class=stdlink" href="{ url_for(
) 'notes.evaluation_list_operations', scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id )
eval_info = "xxx" }">Gérer les opérations</a></span><br>
if E["eval_state"]["evalcomplete"]: """
eval_info = '<span class="eval_info eval_complete">Evaluation prise en compte dans les moyennes</span>'
elif E["eval_state"]["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=E["evaluation_id"])
+ eval_info
+ html_form
+ t
+ "\n".join(C)
) )
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( def _add_eval_columns(
e, evaluation: Evaluation,
eval_state,
evals_poids, evals_poids,
ues, ues,
rows, rows,
@ -678,24 +703,24 @@ def _add_eval_columns(
nb_att = 0 nb_att = 0
sum_notes = 0 sum_notes = 0
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
evaluation_id = e["evaluation_id"] inscrits = evaluation.moduleimpl.formsemestre.etudids_actifs # set d'etudids
e_o = db.session.get(Evaluation, evaluation_id) # XXX en attendant ré-écriture notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
inscrits = e_o.moduleimpl.formsemestre.etudids_actifs # set d'etudids
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
if len(e["jour"]) > 0: if evaluation.date_debut:
titles[evaluation_id] = "%(description)s (%(jour)s)" % e titles[
evaluation.id
] = f"{evaluation.description} ({evaluation.date_debut.strftime('%d/%m/%Y')})"
else: else:
titles[evaluation_id] = "%(description)s " % e titles[evaluation.id] = f"{evaluation.description} "
if e["eval_state"]["evalcomplete"]: if eval_state["evalcomplete"]:
klass = "eval_complete" klass = "eval_complete"
elif e["eval_state"]["evalattente"]: elif eval_state["evalattente"]:
klass = "eval_attente" klass = "eval_attente"
else: else:
klass = "eval_incomplete" klass = "eval_incomplete"
titles[evaluation_id] += " (non prise en compte)" titles[evaluation.id] += " (non prise en compte)"
titles[f"_{evaluation_id}_td_attrs"] = f'class="{klass}"' titles[f"_{evaluation.id}_td_attrs"] = f'class="{klass}"'
for row in rows: for row in rows:
etudid = row["etudid"] etudid = row["etudid"]
@ -712,8 +737,8 @@ def _add_eval_columns(
and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_NEUTRALISE
and val != scu.NOTES_ATTENTE and val != scu.NOTES_ATTENTE
): ):
if e["note_max"] > 0: if evaluation.note_max > 0:
valsur20 = val * 20.0 / e["note_max"] # remet sur 20 valsur20 = val * 20.0 / evaluation.note_max # remet sur 20
else: else:
valsur20 = 0 valsur20 = 0
notes.append(valsur20) # toujours sur 20 pour l'histogramme notes.append(valsur20) # toujours sur 20 pour l'histogramme
@ -731,7 +756,7 @@ def _add_eval_columns(
comment, comment,
) )
else: else:
if (etudid in inscrits) and e["publish_incomplete"]: if (etudid in inscrits) and evaluation.publish_incomplete:
# Note manquante mais prise en compte immédiate: affiche ATT # Note manquante mais prise en compte immédiate: affiche ATT
val = scu.NOTES_ATTENTE val = scu.NOTES_ATTENTE
val_fmt = "ATT" val_fmt = "ATT"
@ -746,11 +771,11 @@ def _add_eval_columns(
) )
if val is None: if val is None:
row[f"_{evaluation_id}_td_attrs"] = f'class="etudabs {cell_class}" ' row[f"_{evaluation.id}_td_attrs"] = f'class="etudabs {cell_class}" '
if not row.get("_css_row_class", ""): if not row.get("_css_row_class", ""):
row["_css_row_class"] = "etudabs" row["_css_row_class"] = "etudabs"
else: else:
row[f"_{evaluation_id}_td_attrs"] = f'class="{cell_class}" ' row[f"_{evaluation.id}_td_attrs"] = f'class="{cell_class}" '
# regroupe les commentaires # regroupe les commentaires
if explanation: if explanation:
if explanation in K: if explanation in K:
@ -763,8 +788,8 @@ def _add_eval_columns(
row.update( row.update(
{ {
evaluation_id: val_fmt, evaluation.id: val_fmt,
"_" + str(evaluation_id) + "_help": explanation, "_" + str(evaluation.id) + "_help": explanation,
# si plusieurs evals seront ecrasés et non affichés: # si plusieurs evals seront ecrasés et non affichés:
"comment": explanation, "comment": explanation,
"expl_key": expl_key, "expl_key": expl_key,
@ -772,36 +797,38 @@ def _add_eval_columns(
} }
) )
row_coefs[evaluation_id] = "coef. %s" % e["coefficient"] row_coefs[evaluation.id] = f"coef. {evaluation.coefficient:g}"
if is_apc: if is_apc:
if fmt == "html": if fmt == "html":
row_poids[evaluation_id] = _mini_table_eval_ue_poids( row_poids[evaluation.id] = _mini_table_eval_ue_poids(
evaluation_id, evals_poids, ues evaluation.id, evals_poids, ues
) )
else: else:
row_poids[evaluation_id] = e_o.get_ue_poids_str() row_poids[evaluation.id] = evaluation.get_ue_poids_str()
if note_sur_20: if note_sur_20:
nmax = 20.0 nmax = 20.0
else: else:
nmax = e["note_max"] nmax = evaluation.note_max
if keep_numeric: if keep_numeric:
row_note_max[evaluation_id] = nmax row_note_max[evaluation.id] = nmax
else: else:
row_note_max[evaluation_id] = "/ %s" % nmax row_note_max[evaluation.id] = f"/ {nmax}"
if nb_notes > 0: if nb_notes > 0:
row_moys[evaluation_id] = scu.fmt_note( row_moys[evaluation.id] = scu.fmt_note(
sum_notes / nb_notes, keep_numeric=keep_numeric sum_notes / nb_notes, keep_numeric=keep_numeric
) )
row_moys[ row_moys[
"_" + str(evaluation_id) + "_help" "_" + str(evaluation.id) + "_help"
] = "moyenne sur %d notes (%s le %s)" % ( ] = "moyenne sur %d notes (%s le %s)" % (
nb_notes, nb_notes,
e["description"], evaluation.description,
e["jour"], evaluation.date_debut.strftime("%d/%m/%Y")
if evaluation.date_debut
else "",
) )
else: else:
row_moys[evaluation_id] = "" row_moys[evaluation.id] = ""
return notes, nb_abs, nb_att # pour histogramme return notes, nb_abs, nb_att # pour histogramme

View File

@ -50,7 +50,6 @@ table.dataTable thead .sorting_desc,
table.dataTable thead .sorting_asc_disabled, table.dataTable thead .sorting_asc_disabled,
table.dataTable thead .sorting_desc_disabled { table.dataTable thead .sorting_desc_disabled {
cursor: pointer; cursor: pointer;
*cursor: hand;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center right; background-position: center right;
} }
@ -83,9 +82,9 @@ table.dataTable tbody tr.selected {
background-color: #b0bed9; background-color: #b0bed9;
} }
table.dataTable tbody th, table.dataTable.gt_table tbody th,
table.dataTable tbody td { table.dataTable.gt_table tbody td {
padding: 8px 10px; padding: 2px 2px;
} }
table.dataTable.row-border tbody th, table.dataTable.row-border tbody th,
@ -138,6 +137,10 @@ table.dataTable.display tbody tr:hover.selected {
background-color: #a9b7d1; background-color: #a9b7d1;
} }
table.dataTable.with-highlight tr:hover td {
background-color: rgba(255, 255, 0, 0.415);
}
table.dataTable.order-column tbody tr > .sorting_1, table.dataTable.order-column tbody tr > .sorting_1,
table.dataTable.order-column tbody tr > .sorting_2, table.dataTable.order-column tbody tr > .sorting_2,
table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.order-column tbody tr > .sorting_3,
@ -368,7 +371,6 @@ table.dataTable td {
.dataTables_wrapper { .dataTables_wrapper {
position: relative; position: relative;
clear: both; clear: both;
*zoom: 1;
zoom: 1; zoom: 1;
} }
@ -408,7 +410,6 @@ table.dataTable td {
text-align: center; text-align: center;
text-decoration: none !important; text-decoration: none !important;
cursor: pointer; cursor: pointer;
*cursor: hand;
color: #333333 !important; color: #333333 !important;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 2px; border-radius: 2px;
@ -760,4 +761,3 @@ table.dataTable.gt_table.gt_left td,
table.dataTable.gt_table.gt_left th { table.dataTable.gt_table.gt_left th {
text-align: left; text-align: left;
} }
scodoc;css

View File

@ -1139,8 +1139,13 @@ a.redlink:hover {
} }
a.discretelink, a.discretelink,
a:discretelink:visited { a.discretelink:visited {
color: black; color: black;
text-decoration: underline;
text-decoration-style: dotted;
}
table.gt_table a.discretelink,
table.gt_table a.discretelink:visited {
text-decoration: none; text-decoration: none;
} }

View File

@ -29,7 +29,7 @@ class TableEtud(tb.Table):
): ):
etuds = etuds or [] etuds = etuds or []
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
classes = classes or ["gt_table", "gt_left"] classes = classes or ["gt_table", "gt_left", "with-highlight"]
super().__init__( super().__init__(
row_class=row_class or RowEtud, row_class=row_class or RowEtud,
classes=classes, classes=classes,

View File

@ -3,7 +3,7 @@
<div class="pageContent"> <div class="pageContent">
{{minitimeline | safe }} {{minitimeline | safe }}
<h2>Assiduité de {{sco.etud.nomprenom}}</h2> <h2>Assiduité de {{sco.etud.html_link_fiche()|safe}}</h2>
<div class="options"> <div class="options">
<input type="checkbox" id="show_pres" name="show_pres" class="memo"><label for="show_pres">afficher les présences</label> <input type="checkbox" id="show_pres" name="show_pres" class="memo"><label for="show_pres">afficher les présences</label>

View File

@ -1641,19 +1641,17 @@ def signal_assiduites_diff():
).build() ).build()
@bp.route("/SignalEvaluationAbs/<int:evaluation_id>/<int:etudid>") @bp.route("/signale_evaluation_abs/<int:evaluation_id>/<int:etudid>")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.AbsChange)
def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None): def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
""" """
Signale l'absence d'un étudiant à une évaluation Signale l'absence d'un étudiant à une évaluation
Si la durée de l'évaluation est inférieur à 1 jour Si la durée de l'évaluation est inférieure à 1 jour
Alors l'absence sera sur la période de l'évaluation l'absence sera sur la période de l'évaluation
Sinon L'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant sinon l'utilisateur sera redirigé vers la page de saisie des absences de l'étudiant
""" """
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
# Récupération de l'évaluation concernée
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut delta: datetime.timedelta = evaluation.date_fin - evaluation.date_debut
@ -1683,9 +1681,9 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
etat=scu.EtatAssiduite.ABSENT, etat=scu.EtatAssiduite.ABSENT,
moduleimpl=evaluation.moduleimpl, moduleimpl=evaluation.moduleimpl,
) )
except ScoValueError as see: except ScoValueError as exc:
# En cas d'erreur # En cas d'erreur
msg: str = see.args[0] msg: str = exc.args[0]
if "Duplication" in msg: if "Duplication" in msg:
msg = """Une autre saisie concerne déjà cette période. msg = """Une autre saisie concerne déjà cette période.
En cliquant sur continuer vous serez redirigé vers la page de En cliquant sur continuer vous serez redirigé vers la page de
@ -1703,12 +1701,12 @@ def signal_evaluation_abs(etudid: int = None, evaluation_id: int = None):
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
duplication="oui", duplication="oui",
) )
raise ScoValueError(msg, dest) from see raise ScoValueError(msg, dest) from exc
db.session.add(assiduite_unique) db.session.add(assiduite_unique)
db.session.commit() db.session.commit()
# on flash pour indiquer que l'absence a bien été créée puis on revient sur la page de l'évaluation # on flash puis on revient sur la page de l'évaluation
flash("L'absence a bien été créée") flash("L'absence a bien été créée")
# rediriger vers la page d'évaluation # rediriger vers la page d'évaluation
return redirect( return redirect(

View File

@ -1,6 +1,6 @@
"""Tests unitaires : bulletins de notes """Tests unitaires : bulletins de notes
Utiliser comme: Utiliser comme:
pytest tests/unit/test_sco_basic.py pytest tests/unit/test_sco_basic.py
Au besoin, créer un base de test neuve: Au besoin, créer un base de test neuve:
@ -69,8 +69,8 @@ def test_bulletin_data_classic(test_client):
min_eval_1 = float(note_eval_1["min"]) min_eval_1 = float(note_eval_1["min"])
max_eval_1 = float(note_eval_1["max"]) max_eval_1 = float(note_eval_1["max"])
# la valeur actuelle est 12.34, on s'assure qu'elle n'est pas extrême: # la valeur actuelle est 12.34, on s'assure qu'elle n'est pas extrême:
assert min_eval_1 > 0 assert min_eval_1 > 0 # 12.34
assert max_eval_1 < 20 assert max_eval_1 < 20 # 12.34
# Saisie note pour changer min/max: # Saisie note pour changer min/max:
# Met le max à 20: # Met le max à 20: