diff --git a/app/models/evaluations.py b/app/models/evaluations.py
index 4eb738cf2c140a43812b0174c151f302cb5c4c11..d7c41030f14a44daeed715c42b434f8a2f802c3b 100644
--- a/app/models/evaluations.py
+++ b/app/models/evaluations.py
@@ -1,7 +1,6 @@
# -*- coding: UTF-8 -*
-"""ScoDoc models: evaluations
-"""
+"""ScoDoc models: evaluations"""
import datetime
from operator import attrgetter
@@ -548,6 +547,25 @@ class Evaluation(models.ScoDocModel):
.all()
)
+ def get_bounds(self) -> tuple[int, int]:
+ """Calcule les notes min et max pour une évaluation."""
+ note_max = self.note_max or 0.0
+ module = self.moduleimpl.module
+ if module.module_type in (
+ scu.ModuleType.STANDARD,
+ scu.ModuleType.RESSOURCE,
+ scu.ModuleType.SAE,
+ ):
+ if self.evaluation_type == Evaluation.EVALUATION_BONUS:
+ note_min, note_max = -20, 20
+ else:
+ note_min = scu.NOTES_MIN
+ elif module.module_type == scu.ModuleType.MALUS:
+ note_min = -20.0
+ else:
+ raise ValueError("Invalid module type") # bug
+ return note_min, note_max
+
class EvaluationUEPoids(models.ScoDocModel):
"""Poids des évaluations (BUT)
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index 386f900304d26810bbad21e92f0aab6a9fffdb63..9e45d0ba770377a92b5cf2693b25c04fc69432fa 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -80,9 +80,11 @@ def _menu_scolarite(
title="Semestre verrouillé",
)
return lockicon # no menu
- if not authuser.has_permission(
- Permission.EtudInscrit
- ) and not authuser.has_permission(Permission.EtudChangeGroups):
+ if (
+ not authuser.has_permission(Permission.EtudInscrit)
+ and not authuser.has_permission(Permission.EtudChangeGroups)
+ and authuser.id not in [resp.id for resp in formsemestre.responsables]
+ ):
return "" # no menu
args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
@@ -106,6 +108,13 @@ def _menu_scolarite(
and authuser.has_permission(Permission.EtudInscrit)
and not locked
)
+ note_enabled = etat_inscription == scu.INSCRIT and (
+ etat_inscription != scu.DEMISSION
+ and (
+ authuser.has_permission(Permission.EditAllNotes)
+ or (authuser.id in [resp.id for resp in formsemestre.responsables])
+ )
+ )
items = [
{
"title": dem_title,
@@ -161,6 +170,12 @@ def _menu_scolarite(
"args": {"etudid": etudid},
"enabled": authuser.has_permission(Permission.EtudInscrit),
},
+ {
+ "title": "Editer toutes les notes",
+ "endpoint": "notes.form_saisie_notes_par_etu",
+ "args": {"etu_id": etudid, "semestre_id": formsemestre.id},
+ "enabled": note_enabled,
+ },
]
return htmlutils.make_menu(
@@ -215,9 +230,9 @@ def fiche_etud(etudid=None):
info[
"modifadresse"
] = f"""<a class="stdlink" href="{
- url_for("scolar.form_change_coordonnees",
- scodoc_dept=g.scodoc_dept, etudid=etudid)
- }">modifier adresse</a>"""
+ url_for("scolar.form_change_coordonnees",
+ scodoc_dept=g.scodoc_dept, etudid=etudid)
+ }">modifier adresse</a>"""
else:
info["modifadresse"] = ""
@@ -260,8 +275,8 @@ def fiche_etud(etudid=None):
grlinks.append(
f"""<a class="discretelink" href="{
url_for('scolar.groups_lists',
- scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
- }" title="Liste du groupe {gr_name}">{gr_name}</a>
+ scodoc_dept=g.scodoc_dept, group_ids=partition['group_id'])
+ }" title="Liste du groupe {gr_name}">{gr_name}</a>
"""
)
grlink = ", ".join(grlinks)
@@ -289,8 +304,8 @@ def fiche_etud(etudid=None):
else f"""
<span class="link_bul_pdf">
<a class="stdlink" href="{
- url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
- }">Tous les bulletins</a>
+ url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
+ }">Tous les bulletins</a>
</span>
"""
)
@@ -301,16 +316,16 @@ def fiche_etud(etudid=None):
] += f"""
<span class="link_bul_pdf">
<a class="stdlink" href="{
- url_for("notes.validation_rcues",
+ url_for("notes.validation_rcues",
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
- }">Visualiser les compétences BUT</a>
+ }">Visualiser les compétences BUT</a>
</span>
"""
info["link_inscrire_ailleurs"] = (
f"""<span class="link_bul_pdf"><a class="stdlink" href="{
- url_for("notes.formsemestre_inscription_with_modules_form",
- scodoc_dept=g.scodoc_dept, etudid=etudid)
- }">Inscrire à un autre semestre</a></span>
+ url_for("notes.formsemestre_inscription_with_modules_form",
+ scodoc_dept=g.scodoc_dept, etudid=etudid)
+ }">Inscrire à un autre semestre</a></span>
"""
if current_user.has_permission(Permission.EtudInscrit)
else ""
@@ -320,26 +335,26 @@ def fiche_etud(etudid=None):
"link_inscrire_ailleurs"
] += f"""
<span class="link_bul_pdf"><a class="stdlink" href="{
- url_for("notes.jury_delete_manual",
+ url_for("notes.jury_delete_manual",
scodoc_dept=g.scodoc_dept, etudid=etudid,
read_only=not can_edit_jury)
- }">{'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury</a></span>
+ }">{'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury</a></span>
"""
info[
"link_bilan_ects"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
- url_for("notes.etud_bilan_ects",
+ url_for("notes.etud_bilan_ects",
scodoc_dept=g.scodoc_dept, etudid=etudid)
- }">ECTS</a></span>"""
+ }">ECTS</a></span>"""
else:
# non inscrit
l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
if current_user.has_permission(Permission.EtudInscrit):
l.append(
f"""<a href="{
- url_for("notes.formsemestre_inscription_with_modules_form",
- scodoc_dept=g.scodoc_dept, etudid=etudid)
+ url_for("notes.formsemestre_inscription_with_modules_form",
+ scodoc_dept=g.scodoc_dept, etudid=etudid)
}">inscrire</a></li>"""
)
l.append("</b></b>")
@@ -488,10 +503,10 @@ def fiche_etud(etudid=None):
validation_dut120_html = (
f"""Diplôme DUT décerné
en <a class="stdlink" href="{
- url_for("notes.formsemestre_status",
- scodoc_dept=g.scodoc_dept,
- formsemestre_id=validation_dut120.formsemestre.id)
- }">S{validation_dut120.formsemestre.semestre_id}</a>
+ url_for("notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=validation_dut120.formsemestre.id)
+ }">S{validation_dut120.formsemestre.semestre_id}</a>
"""
if validation_dut120
else ""
@@ -502,16 +517,16 @@ def fiche_etud(etudid=None):
] = f"""
<div class="section_but">
{render_template(
- "but/cursus_etud.j2",
- cursus=but_cursus,
- scu=scu,
- validation_dut120_html=validation_dut120_html,
- ) if but_cursus else '<span class="pb-config">problème configuration formation BUT</span>'}
+ "but/cursus_etud.j2",
+ cursus=but_cursus,
+ scu=scu,
+ validation_dut120_html=validation_dut120_html,
+ ) if but_cursus else '<span class="pb-config">problème configuration formation BUT</span>'}
<div class="fiche_but_col2">
<div class="link_validation_rcues">
<a class="stdlink" href="{url_for("notes.validation_rcues",
- scodoc_dept=g.scodoc_dept, etudid=etudid,
- formsemestre_id=last_formsemestre.id)}"
+ scodoc_dept=g.scodoc_dept, etudid=etudid,
+ formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="132px"/>
@@ -726,7 +741,7 @@ def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
return {
# infos accessibles à tous:
- "bac_specialite": f"{etud.admission.bac or ''}{(' '+(etud.admission.specialite or '')) if etud.admission.specialite else ''}",
+ "bac_specialite": f"{etud.admission.bac or ''}{(' ' + (etud.admission.specialite or '')) if etud.admission.specialite else ''}",
"annee_bac": etud.admission.annee_bac or "",
# infos protégées par ViewEtudData:
"info_lycee": info_lycee,
@@ -757,15 +772,15 @@ def get_html_annotations_list(etud: Identite) -> list[str]:
for annot in annotations:
del_link = (
f"""<td class="annodel"><a href="{
- url_for("scolar.doSuppressAnnotation",
+ url_for("scolar.doSuppressAnnotation",
scodoc_dept=g.scodoc_dept, etudid=etud.id, annotation_id=annot.id)}">{
- scu.icontag(
- "delete_img",
- border="0",
- alt="suppress",
- title="Supprimer cette annotation",
- )
- }</a></td>"""
+ scu.icontag(
+ "delete_img",
+ border="0",
+ alt="suppress",
+ title="Supprimer cette annotation",
+ )
+ }</a></td>"""
if sco_permissions_check.can_suppress_annotation(annot.id)
else ""
)
@@ -773,7 +788,7 @@ def get_html_annotations_list(etud: Identite) -> list[str]:
author = User.query.filter_by(user_name=annot.author).first()
html_annotations_list.append(
f"""<tr><td><span class="annodate">Le {
- annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
+ annot.date.strftime(scu.DATE_FMT) if annot.date else "?"}
par {author.get_prenomnom() if author else "?"} :
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
"""
@@ -854,8 +869,8 @@ def etud_info_html(etudid, with_photo="1", debug=False):
H = f"""<div class="etud_info_div">
<div class="eid_left">
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{
- url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
- }">{etud.nomprenom}</a></div></div>
+ url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
+ }">{etud.nomprenom}</a></div></div>
<div class="eid_info eid_bac">Bac: <span class="eid_bac">{bac_abbrev}</span></div>
<div class="eid_info eid_parcours">{code_cursus}</div>
"""
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index 79d55c0b65c52f39f1b96f2c46ae6b67a1449af0..0e65b1dca4c1bf90e65f5e68b3d069f52dbc3fd7 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -29,7 +29,6 @@ Formulaire revu en juillet 2016
import html
import time
-
import flask
from flask import g, render_template, url_for
from flask_login import current_user
@@ -47,6 +46,7 @@ from app.models import (
ModuleImpl,
ScolarNews,
Assiduite,
+ NotesNotes,
)
from app.models.etudiants import Identite
@@ -68,6 +68,7 @@ from app.scodoc import sco_undo_notes
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TF
import app.scodoc.sco_utils as scu
+from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
from app.scodoc.sco_utils import ModuleType
from app.views import ScoData
@@ -128,24 +129,10 @@ def check_notes(
etudids_non_inscrits : etudid non inscrits à ce module
(ne considère pas l'inscr. au semestre)
"""
- note_max = evaluation.note_max or 0.0
- module: Module = evaluation.moduleimpl.module
- if module.module_type in (
- scu.ModuleType.STANDARD,
- scu.ModuleType.RESSOURCE,
- scu.ModuleType.SAE,
- ):
- if evaluation.evaluation_type == Evaluation.EVALUATION_BONUS:
- note_min, note_max = -20, 20
- else:
- note_min = scu.NOTES_MIN
- elif module.module_type == ModuleType.MALUS:
- note_min = -20.0
- else:
- raise ValueError("Invalid module type") # bug
+ note_min, note_max = evaluation.get_bounds()
# Vérifie inscription au module (même DEM/DEF)
etudids_inscrits_mod = {
- i.etudid for i in evaluation.moduleimpl.query_inscriptions()
+ i.etudid for i in evaluation.moduleimpl.query_inscriptions().all()
}
valid_notes = []
etudids_invalids = []
@@ -256,7 +243,7 @@ def do_evaluation_set_missing(
"sco_page.j2",
content=f"""
<h2>{diag}</h2>
- <p><a href="{ dest_url }">
+ <p><a href="{dest_url}">
Recommencer</a>
</p>
""",
@@ -313,11 +300,11 @@ def do_evaluation_set_missing(
Revenir au formulaire de saisie des notes</a>
</li>
<li><a class="stdlink" href="{
- url_for(
- "notes.moduleimpl_status",
- scodoc_dept=g.scodoc_dept,
- moduleimpl_id=evaluation.moduleimpl_id,
- )}">Tableau de bord du module</a>
+ url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=evaluation.moduleimpl_id,
+ )}">Tableau de bord du module</a>
</li>
</ul>
""",
@@ -455,11 +442,11 @@ def notes_add(
i.etudid for i in evaluation.moduleimpl.query_inscriptions()
}
# Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF)
- etudids_inscrits_sem, etudids_actifs = (
- evaluation.moduleimpl.formsemestre.etudids_actifs()
- )
+ (
+ etudids_inscrits_sem,
+ etudids_actifs,
+ ) = evaluation.moduleimpl.formsemestre.etudids_actifs()
for etudid, value in notes:
-
if check_inscription:
_check_inscription(etudid, etudids_inscrits_sem, etudids_inscrits_mod)
@@ -610,7 +597,7 @@ def _record_note(
if do_it:
log(
f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={
- etudid}, oldval={oldval}"""
+ etudid}, oldval={oldval}"""
)
cursor.execute(
"""DELETE FROM notes_notes
@@ -652,7 +639,7 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ())
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p>
- <p><a href="{ moduleimpl_status_url }">Continuer</a>
+ <p><a href="{moduleimpl_status_url}">Continuer</a>
</p>
""",
)
@@ -762,11 +749,222 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ())
"sco_page.j2",
content="\n".join(H),
title=page_title,
- javascripts=["js/groups_view.js", "js/saisie_notes.js"],
+ javascripts=[
+ "js/groups_view.js",
+ "js/saisie_notes.js",
+ "js/saisie_notes_par_eval.js",
+ ],
sco=ScoData(formsemestre=modimpl.formsemestre),
)
+# US 1030 - DEB
+class DataSection:
+ """Agrège les données sur un ensemble de module (Ressources, SAEs, Standard, Malus).
+ Nécessaire pour garantir l'ordre de présentation pour les formations en apc.
+ data_modmpl: la liste des modules de la section.
+ """
+
+ def __init__(self, module_type: ModuleType):
+ self.name = scu.MODULE_TYPE_NAMES[module_type]
+ self.data_modimpl = []
+
+ def add_modimpl(self, modimpl: ModuleImpl):
+ self.data_modimpl.append(modimpl)
+
+
+class DataModimpl:
+ """Aggrège les données sur un module pour un étudiant donné.
+ # TODO ?: récupére les notes sur les années précédentes pour aider à la saisie
+ # des cursus adaptés (cursus) (sportifs, étalement sur plusieurs années, etc.).
+ data_evals: la iste des évaluations du module.
+ """
+
+ def __init__(self, etudiant: Identite, modimpl: ModuleImpl):
+ self.etudiant = etudiant
+ self.data_evals = []
+ self.modimpl = modimpl
+ self.modimpl_id = modimpl.id
+ self.module_code = modimpl.module.code
+ self.titre = modimpl.module.titre
+ href = url_for(
+ "notes.moduleimpl_status",
+ scodoc_dept=g.scodoc_dept,
+ moduleimpl_id=modimpl.id,
+ )
+ self.link = (
+ f'<a class="stdlink" href="{href}">{self.module_code} {self.titre}</a>.'
+ )
+ # self.cursus = {}
+ formation_id = modimpl.formsemestre.formation_id
+ # for ins in etudiant.inscriptions():
+ # if ins.formsemestre.formation_id == formation_id:
+ # self.cursus[ins.formsemestre.id] = ins.formsemestre
+
+ def add_eval(self, data_eval: DataSection):
+ self.data_evals.append(data_eval)
+
+
+class DataEval:
+ """Récupère les données sur une évaluation appliquée à un étudiant"""
+
+ def __init__(self, etudid: int, evaluation: Evaluation):
+ self.etudid = etudid
+ self.evaluation = evaluation
+ self.evaluation_id = evaluation.id
+ self.description = evaluation.description
+ self.note_min, self.note_max = evaluation.get_bounds()
+ self.evaluation_type: EvaluationType = evaluation.evaluation_type
+ self.note = get_evaluation_etud_note(evaluation, etudid)
+ # self.data_debut = evaluation.date_debut
+ # self.date_fin = evaluation.date_fin
+ # self.ue_poids = evaluation.get_ue_poids_dict()
+ self.note_value = (
+ ""
+ if self.note is None
+ else scu.fmt_note(self.note.value, fixed_precision_str=False)
+ )
+
+ def titre(self) -> str:
+ """Retourne la chaine Html pour le titre d'une évaluation"""
+ eval_type: str = {
+ 0: "", # EVALUATION_NORMAL:
+ 1: " [rattrapage]", # EVALUATION_RATTRAPAGE
+ 2: " [deuxième session]", # EVALUATION_SESSION2
+ 3: " [bonus]", # EVALUATION_BONUS
+ }[self.evaluation_type]
+ return f"""
+ <a href="{self.link_to_eval()}">
+ {self.description or "évaluation sans titre"}{eval_type}
+ </a>
+ """
+
+ def forminput(self) -> str:
+ return f"""<input type="text" size="5" id="formnotes_note_{self.evaluation_id}"
+ class="note" onkeypress="return enter_focus_next(this, event);"
+ note_min="{self.note_min}" note_max="{self.note_max}" evaluation_id="{self.evaluation_id}"
+ original-value="{self.note_value}" value="{self.note_value}" />"""
+
+ def link_to_eval(self) -> str:
+ """Retourne l'url' vers une évaluation"""
+ return url_for(
+ "notes.evaluation_listenotes",
+ scodoc_dept=g.scodoc_dept,
+ evaluation_id=self.evaluation.id,
+ )
+
+ def history(self):
+ return f"""<span id="formnotes_hist_{self.evaluation_id}">
+ {get_note_history_menu(self.evaluation.id, self.etudid)}</span>"""
+
+
+class DataForm:
+ """
+ Class qui décrit les données d'une page de saisie par étudiant:
+ decision_jury: bool = indique si une décision de jury a été prise pour cet étudiant
+ data_sections: dict[ModuleTYpe, DataSection] = pour chaque section (ressource, SAE, Malus, Standard),
+ la liste de ses modimpl concernés
+ """
+
+ def __init__(self, etudiant: Identite, semestre: FormSemestre, decision_jury: bool):
+ self.etudiant = etudiant
+ self.semestre = semestre
+ self.decision_jury = decision_jury
+ self.data_sections = {}
+
+ def add_section(self, data_section: DataSection):
+ self.data_sections.append(data_section)
+
+ def link_to_etudiant(self) -> str:
+ link: str = url_for(
+ "notes.formsemestre_bulletinetud",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=self.semestre.id,
+ etudid=self.etudiant.etudid,
+ )
+ return f"""
+ <a href="{link}">{self.etudiant.nomprenom}</a>
+ """
+
+
+def get_data(semestre: FormSemestre, etudiant: Identite) -> DataForm:
+ """Récupère les infos pour une page (donc un étudiant (cf ticket 1030))"""
+ res = res_sem.load_formsemestre_results(semestre)
+ data = DataForm(
+ etudiant, semestre, res.etud_has_decision(etudiant.etudid, include_rcues=False)
+ )
+ if semestre.formation.is_apc():
+ for module_type in ModuleType:
+ data.data_sections[module_type] = DataSection(module_type)
+ else:
+ for module_type in [ ModuleType.STANDARD, ModuleType.MALUS]:
+ data.data_sections[module_type] = DataSection(module_type)
+ for modimpl in semestre.modimpls_sorted:
+ module_type = modimpl.module.module_type
+ if res.modimpl_inscr_df[modimpl.id][etudiant.id]:
+ data_modimpl = DataModimpl(etudiant, modimpl)
+ for evaluation in modimpl.evaluations:
+ data_modimpl.add_eval(DataEval(etudiant.etudid, evaluation))
+ data.data_sections[module_type].add_modimpl(data_modimpl)
+ return data
+
+
+def saisie_notes_par_etu(semestre: FormSemestre, etudiant: Identite):
+ # Check access
+ # (admin, respformation, and responsable_id)
+ locked = not semestre.etat
+ # readonly peut être uinutile si l accès à cette page n est pas autorisée au public
+ readonly = locked or not (
+ current_user.has_permission(Permission.EditAllNotes)
+ or (current_user in semestre.responsables)
+ )
+ inscriptions = etudiant.formsemestre_inscriptions
+ inscription = None
+ for ins in inscriptions:
+ if ins.formsemestre_id == semestre.id:
+ inscription = ins
+ # assert ins is not None
+ if ins is None:
+ raise ScoValueError("Tentative d accès à une inscription inexistante") # bug
+ if ins.etat != scu.INSCRIT:
+ raise ValueError(
+ "Etudiant démissionnaire ou défaillant"
+ ) # ne devrait pas être accessible. bug
+ "Formulaire de saisie de note centré sur l'étudiant"
+ data = get_data(semestre, etudiant)
+ if semestre.formation.is_apc():
+ module_type_order = [
+ ModuleType.RESSOURCE,
+ ModuleType.SAE,
+ ModuleType.STANDARD,
+ ModuleType.MALUS,
+ ]
+ else:
+ module_type_order = [
+ ModuleType.STANDARD,
+ ModuleType.MALUS,
+ ]
+ return render_template(
+ "etud/saisie_notes_par_etu.j2",
+ title="Saisie notes par étudiant",
+ javascripts=["js/saisie_notes.js", "js/saisie_notes_par_etu.js"],
+ sco=ScoData(formsemestre=semestre),
+ data=data,
+ readonly=readonly,
+ get_note_history_menu=get_note_history_menu,
+ module_type_order=module_type_order,
+ )
+
+
+# US 1030 - FIN
+
+
+def get_evaluation_etud_note(evaluation: Evaluation, etudid: int) -> float | str | None:
+ return NotesNotes.query.filter_by(
+ evaluation_id=evaluation.id, etudid=etudid
+ ).first()
+
+
def get_sorted_etuds_notes(
evaluation: Evaluation, etudids: list, formsemestre_id: int
) -> list[dict]:
@@ -815,13 +1013,13 @@ def get_sorted_etuds_notes(
if notes_db[etudid]["uid"]
else None
)
- e["explanation"] = (
- f"""{
- notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M")
+ e[
+ "explanation"
+ ] = f"""{
+ notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M")
} par {user.get_nomplogin() if user else '?'
} {(' : ' + notes_db[etudid]["comment"]) if notes_db[etudid]["comment"] else ''}
"""
- )
else:
e["val"] = ""
e["explanation"] = ""
@@ -941,13 +1139,13 @@ def _form_saisie_notes(
etud_classes.append("group-" + str(group_info["group_id"]))
label = f"""<span class="{classdem}">{e["civilite_str"]} {
- scu.format_nomprenom(e, reverse=True)}{observation}</span>"""
+ scu.format_nomprenom(e, reverse=True)}{observation}</span>"""
# Historique des saisies de notes:
explanation = (
""
if disabled
- else f"""<span id="hist_{etudid}">{
+ else f"""<span id="formnotes_hist_{etudid}">{
get_note_history_menu(evaluation.id, etudid)
}</span>"""
)
@@ -1008,7 +1206,7 @@ def _form_saisie_notes(
H.append(tf.getform()) # check and init
H.append(
f"""<a href="{url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
- moduleimpl_id=modimpl.id)
+ moduleimpl_id=modimpl.id)
}" class="btn btn-primary link-terminer">Terminer</a>
"""
)
@@ -1021,14 +1219,14 @@ def _form_saisie_notes(
<div>
<form id="do_evaluation_set_missing" action="{
url_for("notes.do_evaluation_set_missing", scodoc_dept=g.scodoc_dept)
- }" method="POST">
+ }" method="POST">
Mettre les notes manquantes à
<input type="text" size="5" name="value"/>
<input type="submit" value="OK"/>
<input type="hidden" name="evaluation_id" value="{evaluation.id}"/>
<input class="group_ids_str" type="hidden" name="group_ids_str" value="{
",".join([str(x) for x in groups_infos.group_ids])
- }"/>
+ }"/>
<em>ABS indique "absent" (zéro), EXC "excusé" (neutralisées), ATT "attente"</em>
</form>
</div>
@@ -1061,6 +1259,7 @@ def save_notes(
return json_error(403, "modification notes non autorisee pour cet utilisateur")
#
valid_notes, _, _, _, _ = check_notes(notes, evaluation)
+
if valid_notes:
etudids_changed, _, etudids_with_decision, messages = notes_add(
current_user, evaluation.id, valid_notes, comment=comment, do_it=True
@@ -1069,7 +1268,7 @@ def save_notes(
typ=ScolarNews.NEWS_NOTE,
obj=evaluation.moduleimpl_id,
text=f"""Notes dans <a href="{status_url}">{
- evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
+ evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""",
url=status_url,
max_frequency=30 * 60, # 30 minutes
)
@@ -1120,7 +1319,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str:
nv = "" # ne repete pas la valeur de la note courante
else:
# ancienne valeur
- nv = f": {dispnote}"
+ nv = f": {scu.fmt_note(dispnote)}"
first = False
if i["comment"]:
comment = f' <span class="histcomment">{i["comment"]}</span>'
diff --git a/app/static/js/saisie_notes.js b/app/static/js/saisie_notes.js
index 4ed8e15760f2ea88da8d3865551c05133f71a2c2..b79ffffcc8f75b51499b169852ac280815817d15 100644
--- a/app/static/js/saisie_notes.js
+++ b/app/static/js/saisie_notes.js
@@ -1,4 +1,44 @@
-// Formulaire saisie des notes
+// Procedures communes pour formulaires saisie des notes
+/***************************************************
+ Pré-requis pour l'utilisation des fonctionnalités communes à saisie_notes_par_etu et saisie_notes_par_eval
+ * il existe un champ d'id `formnotes_formsemestre_id` qui donne l'id du formsemestre courant
+ * Les champs de saisie de notes:
+ * répondent au sélecteur ("#formnotes .note")
+ * possèdent un id de la forme `formnotes_note_###` (### identifiant étudiant, évaluation ou autre)
+ * possèdent les attributs initialisés `data-last-saved-value`, `data-original-value` `data_modified` = false;
+ * Les champs historiques:
+ * possèdent un id de la forme `hist_###` (### identifiant étudiant, évaluation ou autre)
+ * les fonctions javascript suivantes sont implémentées:
+ * get_id(field): str (retourne le ### pour un champ note donné)
+ * get_evaluation_id(identifiant: str): int
+ * get formsemestre_id(identifiant: str): int
+ * get_note_min(identifiant: str): int
+ * get_note_max(identifiant: str): int
+ * update_jurylink(note_field: element, etudid: int)
+
+ Fonctionnement:
+ Un identifiant (field_id) represente un chaîne caractéristique d'une ligne de saisie.
+ * etudid pour le formulaire de saisie par évaluation
+ * evaluation_id pour le formulaire de saisie par étudiant
+ un attribut data-modified inque si un changement a eu lieu sur une zone de saisie.
+ initialement
+ l attribut data-modified indique toute modification
+ dans le traitement on_blur on vérifie si la valeur a été changée ou pas
+ exemple '15' changée en '15.0'
+ input => data-modifed = true
+ blur => rien n'est fait (la valeur n est pas changée)
+ modification (JMP)
+ exemple '15' changée en '15.0'
+ input => data-modified = false
+ *****************************************************/
+
+
+const note_prefix = "formnotes_note_";
+const hist_prefix = "formnotes_hist_";
+
+function get_formsemestre_id() {
+ return document.getElementById("formnotes_formsemestre_id").getAttribute("value");;
+}
let nbSaving = 0; // nombre de requêtes en cours
@@ -24,52 +64,41 @@ function decSaving() {
}
}
-document.addEventListener("DOMContentLoaded", function () {
- let noteInputs = document.querySelectorAll("#formnotes .note");
+window.addEventListener('beforeunload', function (e) {
+ const noteInputs = document.querySelectorAll("#formnotes .note");
noteInputs.forEach(function (input) {
- input.addEventListener("input", function() {
- this.setAttribute("data-modified", "true");
- });
- input.addEventListener("blur", input.addEventListener("blur", function(event){write_on_blur(event.currentTarget)} ));
- });
-
- var formInputs = document.querySelectorAll("#formnotes input");
- formInputs.forEach(function (input) {
- input.addEventListener("paste", paste_text);
+ if (input.getAttribute("data-modified") === "true" && input.value !== input.getAttribute("data-last-saved-value")) {
+ valid_note.call(input);
+ }
});
-
- var masquerBtn = document.querySelector(".btn_masquer_DEM");
- masquerBtn.addEventListener("click", masquer_DEM);
+ if (nbSaving > 0) {
+ // Display a confirmation dialog
+ const confirmationMessage = 'Des modifications sont en cours de sauvegarde. Êtes-vous sûr de vouloir quitter cette page ?';
+ e.preventDefault(); // Standard for most modern browsers
+ e.returnValue = confirmationMessage; // For compatibility with older browsers
+ return confirmationMessage; // For compatibility with older browsers
+ }
});
-function write_on_blur(elt) {
- if (elt.getAttribute("data-modified") === "true" && elt.value !== elt.getAttribute("data-last-saved-value")) {
- valid_note.call(elt);
- elt.setAttribute("data-modified", "false"); // Reset the modified flag
- }
-}
-
-function is_valid_note(v) {
+function is_valid_note(field_id, v) {
if (!v) return true;
- var note_min = parseFloat(document.querySelector("#eval_note_min").textContent);
- var note_max = parseFloat(document.querySelector("#eval_note_max").textContent);
-
if (!v.match("^-?[0-9]*.?[0-9]*$")) {
return v == "ABS" || v == "EXC" || v == "SUPR" || v == "ATT" || v == "DEM";
} else {
- var x = parseFloat(v);
- return x >= note_min && x <= note_max;
+ const x = parseFloat(v);
+ return x >= get_note_min(field_id) && x <= get_note_max(field_id);
}
}
-function valid_note(e) {
- var v = this.value.trim().toUpperCase().replace(",", ".");
- if (is_valid_note(v)) {
+function valid_note() {
+ const field_id = get_field_id(this)
+ const v = this.value.trim().toUpperCase().replace(",", ".");
+ if (is_valid_note(field_id, v)) {
if (v && v != this.getAttribute("data-last-saved-value")) {
this.className = "note_valid_new";
- const etudid = parseInt(this.getAttribute("data-etudid"));
- save_note(this, v, etudid);
+ const etudid = get_etudid(field_id);
+ save_note(field_id, v);
}
} else {
/* Saisie invalide */
@@ -78,10 +107,33 @@ function valid_note(e) {
}
}
-async function save_note(elem, v, etudid) {
- let evaluation_id = document.querySelector("#formnotes_evaluation_id").getAttribute("value");
- let formsemestre_id = document.querySelector("#formnotes_formsemestre_id").getAttribute("value");
- var scoMsg = document.getElementById("sco_msg");
+function get_field_id(elt) { // la forme des id est ????_#### (où #### est le field_id que l'on cherche)
+ return elt.id.split('_').pop(); // formnotes_note_#### ou eval_####
+}
+
+function get_hist_field(field_id) {
+ return document.getElementById(hist_prefix + field_id);
+}
+
+function get_note_field(field_id) {
+ return document.getElementById(note_prefix + field_id);
+}
+
+function write_on_blur(elt) {
+ if (elt.getAttribute("data-modified") === "true") {
+ if (elt.value !== elt.getAttribute("data-last-saved-value")) {
+ valid_note.call(elt);
+ elt.setAttribute("data-modified", "false"); // Reset the modified flag
+ }
+ }
+}
+
+async function save_note(field_id, v) {
+ const evaluation_id = get_evaluation_id(field_id);
+ const etudid = get_etudid(field_id);
+ const scoMsg = document.getElementById("sco_msg");
+ const note_field = get_note_field(field_id);
+ const hist_field = get_hist_field(field_id);
scoMsg.innerHTML = "en cours...";
scoMsg.style.display = "block";
incSaving(); // update counter to show one more saving in progress
@@ -103,106 +155,51 @@ async function save_note(elem, v, etudid) {
sco_message("Erreur: valeur non enregistrée");
} else {
const data = await response.json();
- var scoMsg = document.getElementById("sco_msg");
scoMsg.style.display = "none";
if (data.etudids_changed.length > 0) {
sco_message("enregistré");
- elem.className = "note_saved";
+ note_field.className = "note_saved";
// Il y avait une decision de jury ?
if (data.etudids_with_decision.includes(etudid)) {
- if (v !== elem.getAttribute("data-orig-value")) {
- var juryLink = document.getElementById("jurylink_" + etudid);
- juryLink.innerHTML = `<a href="${SCO_URL}Notes/formsemestre_validation_etud_form?formsemestre_id=${formsemestre_id}&etudid=${etudid}"
- >mettre à jour décision de jury</a>`;
- } else {
- var juryLink = document.getElementById("jurylink_" + etudid);
- juryLink.innerHTML = "";
- }
+ update_jurylink(field_id, v);
}
// Mise à jour menu historique
if (data.history_menu[etudid]) {
- var historyElem = document.getElementById("hist_" + etudid);
- historyElem.innerHTML = data.history_menu[etudid];
+ hist_field.innerHTML = data.history_menu[etudid];
}
- elem.setAttribute("data-last-saved-value", v);
+ note_field.setAttribute("data-last-saved-value", v);
}
}
} catch (error) {
console.error("Fetch error:", error);
sco_message("Erreur réseau: valeur non enregistrée");
} finally {
- decSaving(); // update counter to show one saving in progress less. May re-enable 'Terminer' button
+ decSaving(); // mise à jour du nombre de requêtes en instance. si devient nul, réactive le lien 'Terminer'
}
}
-// Set up the beforeunload event listener
-window.addEventListener('beforeunload', function (e) {
- let noteInputs = document.querySelectorAll("#formnotes .note");
- noteInputs.forEach(function (input) {
- if (input.getAttribute("data-modified") === "true" && input.value !== input.getAttribute("data-last-saved-value")) {
- valid_note.call(input);
- }
- });
- if (nbSaving > 0) {
- // Display a confirmation dialog
- const confirmationMessage = 'Des modifications sont en cours de sauvegarde. Êtes-vous sûr de vouloir quitter cette page ?';
- e.preventDefault(); // Standard for most modern browsers
- e.returnValue = confirmationMessage; // For compatibility with older browsers
- return confirmationMessage; // For compatibility with older browsers
- }
-});
-
-function change_history(e) {
- let opt = e.selectedOptions[0];
- let val = opt.getAttribute("data-note");
- const etudid = parseInt(e.getAttribute("data-etudid"));
- // le input associé a ce menu:
- let input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
- input_elem.value = val;
- save_note(input_elem, val, etudid);
+function make_jurylink(field_id) {
+ const formsemestre_id = get_formsemestre_id();
+ const etudid = get_etudid(field_id);
+ const href = `${SCO_URL}Notes/formsemestre_validation_etud_form?formsemestre_id=${formsemestre_id}&etudid=${etudid}`;
+ return '<a href="' + href +'">mettre à jour décision de jury</a>';
}
-// Contribution S.L.: copier/coller des notes
-
-function paste_text(event) {
- event.stopPropagation();
- event.preventDefault();
- var clipb = event.clipboardData;
- var data = clipb.getData("Text");
- var list = data.split(/\r\n|\r|\n|\t| /g);
- var currentInput = event.currentTarget;
- var masquerDEM = document
- .querySelector("body")
- .classList.contains("masquer_DEM");
-
- for (var i = 0; i < list.length; i++) {
- if (!currentInput.disabled) { // skip DEM
- currentInput.value = list[i];
- }
- // --- trigger blur
- currentInput.setAttribute("data-modified", "true");
- var evt = new Event("blur", { bubbles: true, cancelable: true});
- currentInput.dispatchEvent(evt);
- // --- next input
- var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
- while (
- sibbling &&
- (sibbling.style.display == "none" ||
- (masquerDEM && sibbling.classList.contains("etud_dem")))
- ) {
- sibbling = sibbling.nextElementSibling;
- }
- if (sibbling) {
- currentInput = sibbling.querySelector("input");
- if (!currentInput) {
- return;
+function change_history(e) {
+ const opt = e.selectedOptions[0];
+ const val = opt.getAttribute("data-note");
+ if (val != '') {
+ const field_id = get_field_id(e.parentElement);
+ // le input associé a ce menu:
+ const input = get_note_field(field_id)
+ save_note(field_id, val);
+ input.value = val
+ // gère le style de l'input (note_saved ou pas selon que la valeur de la note a changé par rapport à la valeur initiale)
+ if (val == input.getAttribute("data-orig-value")) {
+ input.classList.remove("note_saved")
+ } else {
+ input.classList.add("note_saved");
}
- } else {
- return;
- }
}
}
-function masquer_DEM() {
- document.querySelector("body").classList.toggle("masquer_DEM");
-}
diff --git a/app/static/js/saisie_notes_par_etu.js b/app/static/js/saisie_notes_par_etu.js
new file mode 100644
index 0000000000000000000000000000000000000000..da7dfe0b5f9a1b8513afc911c5726d5e839ea691
--- /dev/null
+++ b/app/static/js/saisie_notes_par_etu.js
@@ -0,0 +1,73 @@
+/*
+ * Gestion des formulaires de note
+ */
+
+// Formulaire saisie de notes pour une évaluation
+
+const form_etudid = parseInt(document.getElementById("etudiant_id").getAttribute("value"));
+
+function get_note_min(field_id) {
+ const note_field = get_note_field(field_id);
+ return parseFloat(note_field.getAttribute("note_min"));
+}
+
+function get_note_max(field_id) {
+ const note_field = get_note_field(field_id);
+ return parseFloat(note_field.getAttribute("note_max"));
+}
+
+function get_evaluation_id(field_id) {
+ const note_field = get_note_field(field_id);
+ return parseInt(note_field.getAttribute("evaluation_id"));
+}
+
+function get_etudid(field_id) {
+ return parseInt(form_etudid);
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+ const noteInputs = document.querySelectorAll("#formnotes .note");
+ noteInputs.forEach(function (input) {
+ input.addEventListener("input", function() {
+ if (this.value !== this.getAttribute("data-last-saved-value")) {
+ this.setAttribute("data-modified", "true");
+ }
+ });
+ input.addEventListener("blur", input.addEventListener("blur", function(event){
+ write_on_blur(event.currentTarget)
+ }));
+ });
+});
+
+function is_enter_key(event) {
+ if (event.keyCode == 13) return true;
+ if (event.which == 13) return true;
+ if (event.charCode == 13) return true;
+ return false;
+}
+
+function find_next_field(current_element) {
+ const form_input = Array.from(document.querySelectorAll("#formnotes input"))
+ .filter(elem => ! elem.disabled)
+ .filter(elem => $(elem).is(':visible'));
+ const current_index = form_input.indexOf(current_element)
+ return ((current_index == form_input.length -1) ? form_input[current_index] : form_input[current_index+1]);
+}
+function enter_focus_next (elem, event) {
+ if (is_enter_key(event)) {
+ let next_elem = find_next_field(elem);
+ if (next_elem != elem) {
+ next_elem.focus();
+ return true;
+ } else {
+ elem.blur();
+ return false;
+ }
+ }
+}
+
+function update_jurylink(evaluation_id, v) {
+ const note_field = get_note_field(evaluation_id);
+ const jurylink = document.getElementById('jurylink');
+ jurylink.innerHTML = make_jurylink(evaluation_id);
+}
\ No newline at end of file
diff --git a/app/static/js/saisie_notes_par_eval.js b/app/static/js/saisie_notes_par_eval.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c188e35c26069c2e5ba0dba30a05b2eb4c30532
--- /dev/null
+++ b/app/static/js/saisie_notes_par_eval.js
@@ -0,0 +1,95 @@
+// Formulaire saisie de notes pour une évaluation
+
+const note_min = parseFloat(document.querySelector("#eval_note_min").textContent);
+const note_max = parseFloat(document.querySelector("#eval_note_max").textContent);
+const form_evaluation_id = parseInt(document.getElementById("formnotes_evaluation_id").getAttribute("value"));
+
+function get_note_min(field_id) {
+ return note_min;
+}
+
+function get_note_max(field_id) {
+ return note_max;
+}
+
+function get_etudid(field_id) {
+ return parseInt(get_note_field(field_id).getAttribute("data-etudid"));
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+ const noteInputs = document.querySelectorAll("#formnotes .note");
+ noteInputs.forEach(function (input) {
+ console.log(get_field_id(input));
+ input.addEventListener("input", function() {
+ this.setAttribute("data-modified", "true");
+ });
+ input.addEventListener("blur", input.addEventListener("blur", function(event){write_on_blur(event.currentTarget)} ));
+ });
+
+ const formInputs = document.querySelectorAll("#formnotes input");
+ formInputs.forEach(function (input) {
+ input.addEventListener("paste", paste_text);
+ });
+
+ const masquerBtn = document.querySelector(".btn_masquer_DEM");
+ masquerBtn.addEventListener("click", masquer_DEM);
+});
+
+function get_evaluation_id(field_id) {
+ return form_evaluation_id
+}
+
+// Contribution S.L.: copier/coller des notes
+
+function paste_text(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ var clipb = event.clipboardData;
+ var data = clipb.getData("Text");
+ var list = data.split(/\r\n|\r|\n|\t| /g);
+ var currentInput = event.currentTarget;
+ var masquerDEM = document
+ .querySelector("body")
+ .classList.contains("masquer_DEM");
+
+ for (var i = 0; i < list.length; i++) {
+ if (!currentInput.disabled) { // skip DEM
+ currentInput.value = list[i];
+ }
+ // --- trigger blur
+ currentInput.setAttribute("data-modified", "true");
+ var evt = new Event("blur", { bubbles: true, cancelable: true});
+ currentInput.dispatchEvent(evt);
+ // --- next input
+ var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
+ while (
+ sibbling &&
+ (sibbling.style.display == "none" ||
+ (masquerDEM && sibbling.classList.contains("etud_dem")))
+ ) {
+ sibbling = sibbling.nextElementSibling;
+ }
+ if (sibbling) {
+ currentInput = sibbling.querySelector("input");
+ if (!currentInput) {
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+}
+
+function masquer_DEM() {
+ document.querySelector("body").classList.toggle("masquer_DEM");
+}
+
+function update_jurylink(etudid, v) {
+ const juryLink = document.getElementById("jurylink_" + etudid);
+ const note_field = get_note_field(etudid)
+ if (v !== note_field.getAttribute("data-orig-value")) {
+ juryLink.innerHTML = make_jurylink(etudid);
+ } else {
+ juryLink.innerHTML = "";
+ }
+}
\ No newline at end of file
diff --git a/app/templates/etud/saisie_notes_par_etu.j2 b/app/templates/etud/saisie_notes_par_etu.j2
new file mode 100644
index 0000000000000000000000000000000000000000..b0c6d6948cfb6bc228862096c462a73f1cfd2b04
--- /dev/null
+++ b/app/templates/etud/saisie_notes_par_etu.j2
@@ -0,0 +1,109 @@
+{% extends 'sco_page.j2' %}
+
+{% block styles %}
+ {{super()}}
+ <link href="{{scu.STATIC_DIR}}/css/saisie_notes_par_etu.css" rel="stylesheet" type="text/css" />
+ <style>
+ #formnotes {
+ display:inline-grid;
+ grid-template-columns: 40px 40px minmax(350px, 1fr) 70px minmax(200px, 1fr);
+ align-items: baseline;
+ }
+ #formnotes .section {
+ grid-column: 1 / span 5;
+ border-bottom: solid black;
+ margin-bottom: 5px;
+ font-weight: bold;
+ }
+ #formnotes .module {
+ grid-column: 2 / span 4;
+ margin-bottom: 3px;
+ background-color: lightsteelblue;
+ padding-left: 5px;
+ }
+ #formnotes .label {
+ grid-column: 3;
+ }
+ #formnotes .input { grid-column: 4 }
+ #formnotes .history { grid-column: 5;}
+ #formnotes .long_input { grid-column: 4 / span 2 }
+ #formnotes .validation { grid-column: 2 / span 2 }
+ </style>
+{% endblock %}
+
+{% block app_content %}
+ <div id="saisie_notes_par_etu" class="table-container">
+ <span class="titre">{{title}}</span>
+ <h3> Saisie des notes de l'étudiant : {{data.link_to_etudiant() | safe }} </h3>
+ {% if data.decision_jury %}
+ <div class="warning">
+ <ul class="tf-msg">
+ <li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour
+ cet étudiant. Après changement des notes, vérifiez la situation !</li>
+ </ul>
+ </div>
+ {% endif %}
+ <div id="formnotes">
+ <input type="hidden" id="etudiant_id" value="{{data.etudiant.id}}" />
+ <input type="hidden" id="formnotes_formsemestre_id" value="{{data.semestre.id}}" />
+ {% if data.decision_jury %}
+ {% endif %}
+ <div class="label">commentaire</div>
+ <div class="long_input">
+ <input type="text" id="formnotes_comment" name="comment" class="input" size="44" onkeypress="return enter_focus_next(this, event);">
+ </div>
+ {% for module_type in module_type_order %}
+ {% if data.data_sections[module_type].data_modimpl %}
+ <div class="section">{{ data.data_sections[module_type].name }}</div>
+ {% for data_modimpl in data.data_sections[module_type].data_modimpl %}
+ <div class="module">{{data_modimpl.link | safe}}</div>
+ {% for data_eval in data_modimpl.data_evals %}
+ <div class="label">
+ {{ data_eval.titre() | safe }}
+{# <input type="hidden" id="date_debut_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_debut }}">#}
+{# <input type="hidden" id="date_fin_{{ data_eval.evaluation_id }}" value="{{ data_eval.date_fin }}">#}
+{# {% for ue in data_eval.ue_poids %}#}
+{# <div id="poids_{{ data_eval.evaluation_id }}_{{ ue }}">#}
+{# <input type="hidden" class="ue" value="{{ ue }}">#}
+{# <input type="hidden" class="evaluation" value="{{ data_eval.evaluation_id }}">#}
+{# <input type="hidden" value="{{ data_eval.ue_poids[ue] }}">#}
+{# </div>#}
+{# {% endfor %}#}
+{# </input>#}
+ </div>
+ <div class="input">
+ {{ data_eval.forminput() | safe }}
+ </div>
+ <div class="history">
+ {{ data_eval.history() | safe }}
+ </div>
+ {% endfor %}
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ <div class="validation">
+ <a href="/ScoDoc/INFO/Scolarite/Notes/moduleimpl_status?moduleimpl_id=21574"
+ class="btn btn-primary link-terminer"
+ style="pointer-events: unset;
+ color: unset; text-decoration: unset;
+ cursor: unset;">Terminer</a>
+ {% if data.decision_jury %}
+ <span id="jurylink" class="jurylink"></span>
+ {% endif %}
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="sco_help">
+ <p>
+ Les modifications sont enregistrées au fur et à mesure.
+ </p>
+ <h4> Codes spéciaux: </h4>
+ <ul>
+ <li> ABS: absent (compte comme un zéro) </li>
+ <li> EXC: excusé (note neutralisée) </li>
+ <li> SUPR: pour supprimer une note existante </li>
+ <li> ATT: note en attente (permet de publier une évaluation avec des notes manquantes) </li>
+ </ul>
+ </div>
+{% endblock %}
diff --git a/app/views/notes.py b/app/views/notes.py
index 9a223f5059d5291e09f6f55a2db23a50e6c27b99..7dfa03bd23ff942b820e08bf4a21d4231460cf6d 100644
--- a/app/views/notes.py
+++ b/app/views/notes.py
@@ -1816,6 +1816,22 @@ def form_saisie_notes(evaluation_id: int):
return sco_saisie_notes.saisie_notes(evaluation, group_ids)
+# modification US 1030 - DEB
+
+
+@bp.route("/form_saisie_notes_par_etu/<int:semestre_id>/<int:etu_id>")
+@scodoc
+@permission_required(Permission.EnsView)
+def form_saisie_notes_par_etu(etu_id: int, semestre_id: int):
+ "Formulaire de saisie des notes centré sur l'étudiant"
+ etudiant = Identite.get_etud(etu_id)
+ semestre = FormSemestre.get_formsemestre(semestre_id)
+ return sco_saisie_notes.saisie_notes_par_etu(semestre, etudiant)
+
+
+# modification US 1030 - FIN
+
+
@bp.route("/formsemestre_import_notes/<int:formsemestre_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView) # controle contextuel