ScoDoc/app/scodoc/sco_evaluation_edit.py

435 lines
16 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@gmail.com
#
##############################################################################
"""Formulaire ajout/édition d'une évaluation
"""
import datetime
import time
import flask
from flask import g, render_template, request, url_for
from flask_login import current_user
from app import db
from app.models import Evaluation, Module, ModuleImpl
from app.models.evaluations import heure_to_time, check_and_convert_evaluation_args
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_evaluations
from app.scodoc import sco_preferences
def evaluation_create_form(
moduleimpl_id=None,
evaluation_id=None,
edit=False,
page_title="Évaluation",
):
"Formulaire création/édition d'une évaluation (pas de ses notes)"
evaluation: Evaluation
if evaluation_id is not None:
evaluation = db.session.get(Evaluation, evaluation_id)
if evaluation is None:
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
moduleimpl_id = evaluation.moduleimpl_id
#
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
formsemestre_id = modimpl.formsemestre_id
formsemestre = modimpl.formsemestre
module: Module = modimpl.module
sem_ues = formsemestre.get_ues(with_sport=False)
is_malus = module.module_type == ModuleType.MALUS
is_apc = module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE)
preferences = sco_preferences.SemPreferences(formsemestre.id)
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
#
if not modimpl.can_edit_evaluation(current_user):
return f"""
{html_sco_header.sco_header()}
<h2>Opération non autorisée</h2>
<p>Modification évaluation impossible pour {current_user.get_nomplogin()}</p>
<p><a href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}" class="stdlink">Revenir</a>
</p>
{html_sco_header.sco_footer()}
"""
if not edit:
# création nouvel
if moduleimpl_id is None:
raise ValueError("missing moduleimpl_id parameter")
numeros = [(e.numero or 0) for e in modimpl.evaluations]
initvalues = {
"jour": time.strftime(scu.DATE_FMT, time.localtime()),
"note_max": 20,
"numero": (max(numeros) + 1) if numeros else 0,
"publish_incomplete": is_malus,
"visibulletin": 1,
}
submitlabel = "Créer cette évaluation"
action = "Création d'une évaluation"
link = ""
else:
# édition données existantes
# setup form init values
if evaluation_id is None:
raise ValueError("missing evaluation_id parameter")
initvalues = evaluation.to_dict()
moduleimpl_id = initvalues["moduleimpl_id"]
submitlabel = "Modifier l'évaluation"
action = "Modification d'une évaluation"
link = ""
# Note maximale actuelle dans cette éval ?
etat = sco_evaluations.do_evaluation_etat(evaluation_id)
if etat["maxi_num"] is not None:
min_note_max = max(scu.NOTES_PRECISION, etat["maxi_num"])
else:
min_note_max = scu.NOTES_PRECISION
#
if min_note_max > scu.NOTES_PRECISION:
min_note_max_str = scu.fmt_note(min_note_max)
else:
min_note_max_str = "0"
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
vals = scu.get_request_args()
#
ue_coef_dict = modimpl.module.get_ue_coef_dict()
if is_apc: # BUT: poids vers les UE
if edit:
if evaluation.set_default_poids():
db.session.commit()
ue_poids_dict = evaluation.get_ue_poids_dict()
for ue in sem_ues:
initvalues[f"poids_{ue.id}"] = ue_poids_dict[ue.id]
else:
for ue in sem_ues:
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
if coef_ue > 0:
poids = 1.0 # par défaut au départ
else:
poids = 0.0
initvalues[f"poids_{ue.id}"] = poids
# Blocage
if edit:
initvalues["blocked"] = evaluation.is_blocked()
initvalues["blocked_until"] = (
evaluation.blocked_until.strftime(scu.DATE_FMT)
if evaluation.blocked_until
and evaluation.blocked_until < Evaluation.BLOCKED_FOREVER
else ""
)
#
form = [
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
("numero", {"default": initvalues["numero"], "input_type": "hidden"}),
(
"jour",
{
"input_type": "datedmy",
"title": "Date",
"size": 12,
"explanation": "date de l'examen, devoir ou contrôle",
},
),
(
"heure_debut",
{
"title": "Heure de début",
"explanation": "heure du début de l'épreuve",
"input_type": "time",
},
),
(
"heure_fin",
{
"title": "Heure de fin",
"explanation": "heure de fin de l'épreuve",
"input_type": "time",
},
),
]
if is_malus: # pas de coefficient
form.append(("coefficient", {"input_type": "hidden", "default": "1."}))
elif not is_apc: # modules standard hors BUT
form.append(
(
"coefficient",
{
"size": 6,
"type": "float", # peut être négatif (!)
"explanation": """coef. dans le module (choisi librement par
l'enseignant, non utilisé pour rattrapage, 2ème session et bonus)""",
"allow_null": False,
},
)
)
form += [
(
"note_max",
{
"size": 4,
"type": "float",
"title": "Notes de 0 à",
"explanation": f"""barème (note max actuelle: {min_note_max_str}).""",
"allow_null": False,
"max_value": scu.NOTES_MAX,
"min_value": min_note_max,
},
),
(
"description",
{
"size": 36,
"type": "text",
"explanation": """type d'évaluation, apparait sur le bulletins longs.
Exemples: "contrôle court", "examen de TP", "examen final".""",
},
),
(
"visibulletin",
{
"input_type": "boolcheckbox",
"title": "Visible sur bulletins",
"explanation": "(pour les bulletins en version intermédiaire/passerelle)",
},
),
(
"publish_incomplete",
{
"input_type": "boolcheckbox",
"title": "Prise en compte immédiate",
"explanation": """notes utilisées même si incomplètes (dangereux,
à n'utiliser que dans des cas particuliers
<a target="_blank" rel="noopener noreferrer"
href="https://scodoc.org/Evaluation/#pourquoi-eviter-dutiliser-prise-en-compte-immediate"
>voir la documentation</a>
)
""",
},
),
(
"evaluation_type",
{
"input_type": "menu",
"title": "Modalité",
"allowed_values": Evaluation.VALID_EVALUATION_TYPES,
"type": "int",
"labels": (
"Normale",
"Rattrapage (remplace si meilleure note)",
"Deuxième session (remplace toujours)",
(
"Bonus "
+ (
"(pondéré par poids et ajouté aux moyennes de ce module)"
if is_apc
else "(ajouté à la moyenne de ce module)"
)
),
),
},
),
]
if is_apc: # ressources et SAÉs
form += [
(
"coefficient",
{
"size": 6,
"type": "float",
"explanation": """importance de l'évaluation (multiplie les poids ci-dessous).
Non utilisé pour les bonus.""",
"allow_null": False,
"dom_id": "evaluation-edit-coef",
},
),
]
# Liste des UE utilisées dans des modules de ce semestre:
if sem_ues and not can_edit_poids:
form.append(
(
"sep_poids",
{
"input_type": "separator",
"title": """
<div class="warning-light">les poids ne sont pas modifiables (voir réglage paramétrage)
</div>""",
},
)
)
for ue in sem_ues:
coef_ue = ue_coef_dict.get(ue.id, 0.0)
form.append(
(
f"poids_{ue.id}",
{
"title": f"Poids {ue.acronyme}",
"size": 2,
"type": "float",
"explanation": f"""
<span class="eval_coef_ue" title="coef. du module dans cette UE">({
"coef. mod.:" +str(coef_ue) if coef_ue
else "ce module n'a pas de coef. dans cette UE"
})</span>
<span class="eval_coef_ue_titre">{ue.titre or ''}</span>
""",
"allow_null": False,
# ok si poids nul ou coef vers l'UE nul:
"validator": lambda val, field: (not val)
or ue_coef_dict.get(int(field[len("poids_") :]), 0.0) != 0,
"enabled": (coef_ue != 0 or initvalues[f"poids_{ue.id}"] != 0.0)
and can_edit_poids,
},
),
)
# Bloquage / date prise en compte
form += [
(
"blocked",
{
"input_type": "boolcheckbox",
"title": "Bloquer la prise en compte",
"explanation": """empêche la prise en compte
(ne sera pas visible sur les bulletins ni dans les tableaux)""",
"dom_id": "evaluation-edit-blocked",
},
),
(
"blocked_until",
{
"input_type": "datedmy",
"title": "Date déblocage",
"size": 12,
"explanation": "sera débloquée à partir de cette date",
},
),
]
tf = TrivialFormulator(
request.base_url,
vals,
form,
cancelbutton="Annuler",
submitlabel=submitlabel,
initvalues=initvalues,
readonly=False,
)
dest_url = url_for(
"notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id
)
H = [
f"""<h3>{action} en
{scu.MODULE_TYPE_NAMES[module.module_type]} <a class="stdlink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">{module.code or "module sans code"} {
module.titre or module.abbrev or "(sans titre)"
}</a> {link}</h3>
"""
]
if tf[0] == 0:
head = html_sco_header.sco_header(page_title=page_title)
return (
head
+ "\n".join(H)
+ "\n"
+ tf[1]
+ render_template(
"scodoc/help/evaluations.j2", is_apc=is_apc, modimpl=modimpl
)
+ render_template("sco_timepicker.j2")
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
# form submission
args = tf[2]
# modifie le codage des dates
# (nb: ce formulaire ne permet de créer que des évaluation sur la même journée)
date_debut = scu.convert_fr_date(args["jour"]) if args.get("jour") else None
args["date_debut"] = date_debut
args["date_fin"] = date_debut # même jour
args.pop("jour", None)
if date_debut and args.get("heure_debut"):
try:
heure_debut = heure_to_time(args["heure_debut"])
except ValueError as exc:
raise ScoValueError("Heure début invalide") from exc
args["date_debut"] = datetime.datetime.combine(date_debut, heure_debut)
args.pop("heure_debut", None)
# note: ce formulaire ne permet de créer que des évaluations
# avec debut et fin sur le même jour.
if date_debut and args.get("heure_fin"):
try:
heure_fin = heure_to_time(args["heure_fin"])
except ValueError as exc:
raise ScoValueError("Heure fin invalide") from exc
args["date_fin"] = datetime.datetime.combine(date_debut, heure_fin)
args.pop("heure_fin", None)
# Blocage:
if args.get("blocked"):
if args.get("blocked_until"):
try:
args["blocked_until"] = datetime.datetime.strptime(
args["blocked_until"], scu.DATE_FMT
)
except ValueError as exc:
raise ScoValueError("Date déblocage (j/m/a) invalide") from exc
else: # bloquage coché sans date
args["blocked_until"] = Evaluation.BLOCKED_FOREVER
else: # si pas coché, efface date déblocage
args["blocked_until"] = None
#
if edit:
check_and_convert_evaluation_args(args, modimpl)
evaluation.from_dict(args)
else:
# création d'une evaluation
evaluation = Evaluation.create(moduleimpl=modimpl, **args)
db.session.add(evaluation)
db.session.commit()
evaluation_id = evaluation.id
if is_apc:
# Set poids
evaluation = db.session.get(Evaluation, evaluation_id)
for ue in sem_ues:
evaluation.set_ue_poids(ue, tf[2][f"poids_{ue.id}"])
db.session.add(evaluation)
db.session.commit()
sco_cache.invalidate_formsemestre(evaluation.moduleimpl.formsemestre.id)
return flask.redirect(dest_url)