Skip to content
Snippets Groups Projects
Commit edc73b94 authored by Emmanuel Viennet's avatar Emmanuel Viennet
Browse files

Merge pull request 'resolution WIP: t1030' (#1060) from...

Merge pull request 'resolution WIP: t1030' (#1060) from jmplace/ScoDoc-Lille:solution_t1030 into master

Reviewed-on: https://git.scodoc.org/ScoDoc/ScoDoc/pulls/1060
parents 0f2b0dc8 c4b6379b
Branches
No related tags found
No related merge requests found
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""ScoDoc models: evaluations """ScoDoc models: evaluations"""
"""
import datetime import datetime
from operator import attrgetter from operator import attrgetter
...@@ -548,6 +547,25 @@ class Evaluation(models.ScoDocModel): ...@@ -548,6 +547,25 @@ class Evaluation(models.ScoDocModel):
.all() .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): class EvaluationUEPoids(models.ScoDocModel):
"""Poids des évaluations (BUT) """Poids des évaluations (BUT)
......
...@@ -80,9 +80,11 @@ def _menu_scolarite( ...@@ -80,9 +80,11 @@ def _menu_scolarite(
title="Semestre verrouillé", title="Semestre verrouillé",
) )
return lockicon # no menu return lockicon # no menu
if not authuser.has_permission( if (
Permission.EtudInscrit not authuser.has_permission(Permission.EtudInscrit)
) and not authuser.has_permission(Permission.EtudChangeGroups): and not authuser.has_permission(Permission.EtudChangeGroups)
and authuser.id not in [resp.id for resp in formsemestre.responsables]
):
return "" # no menu return "" # no menu
args = {"etudid": etudid, "formsemestre_id": formsemestre.id} args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
...@@ -106,6 +108,13 @@ def _menu_scolarite( ...@@ -106,6 +108,13 @@ def _menu_scolarite(
and authuser.has_permission(Permission.EtudInscrit) and authuser.has_permission(Permission.EtudInscrit)
and not locked 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 = [ items = [
{ {
"title": dem_title, "title": dem_title,
...@@ -161,6 +170,12 @@ def _menu_scolarite( ...@@ -161,6 +170,12 @@ def _menu_scolarite(
"args": {"etudid": etudid}, "args": {"etudid": etudid},
"enabled": authuser.has_permission(Permission.EtudInscrit), "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( return htmlutils.make_menu(
......
...@@ -29,7 +29,6 @@ Formulaire revu en juillet 2016 ...@@ -29,7 +29,6 @@ Formulaire revu en juillet 2016
import html import html
import time import time
import flask import flask
from flask import g, render_template, url_for from flask import g, render_template, url_for
from flask_login import current_user from flask_login import current_user
...@@ -47,6 +46,7 @@ from app.models import ( ...@@ -47,6 +46,7 @@ from app.models import (
ModuleImpl, ModuleImpl,
ScolarNews, ScolarNews,
Assiduite, Assiduite,
NotesNotes,
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
...@@ -68,6 +68,7 @@ from app.scodoc import sco_undo_notes ...@@ -68,6 +68,7 @@ from app.scodoc import sco_undo_notes
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TF from app.scodoc.TrivialFormulator import TF
import app.scodoc.sco_utils as scu 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 json_error
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.views import ScoData from app.views import ScoData
...@@ -128,24 +129,10 @@ def check_notes( ...@@ -128,24 +129,10 @@ def check_notes(
etudids_non_inscrits : etudid non inscrits à ce module etudids_non_inscrits : etudid non inscrits à ce module
(ne considère pas l'inscr. au semestre) (ne considère pas l'inscr. au semestre)
""" """
note_max = evaluation.note_max or 0.0 note_min, note_max = evaluation.get_bounds()
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
# Vérifie inscription au module (même DEM/DEF) # Vérifie inscription au module (même DEM/DEF)
etudids_inscrits_mod = { etudids_inscrits_mod = {
i.etudid for i in evaluation.moduleimpl.query_inscriptions() i.etudid for i in evaluation.moduleimpl.query_inscriptions().all()
} }
valid_notes = [] valid_notes = []
etudids_invalids = [] etudids_invalids = []
...@@ -455,11 +442,11 @@ def notes_add( ...@@ -455,11 +442,11 @@ def notes_add(
i.etudid for i in evaluation.moduleimpl.query_inscriptions() i.etudid for i in evaluation.moduleimpl.query_inscriptions()
} }
# Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF) # 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: for etudid, value in notes:
if check_inscription: if check_inscription:
_check_inscription(etudid, etudids_inscrits_sem, etudids_inscrits_mod) _check_inscription(etudid, etudids_inscrits_sem, etudids_inscrits_mod)
...@@ -762,11 +749,222 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ()) ...@@ -762,11 +749,222 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ())
"sco_page.j2", "sco_page.j2",
content="\n".join(H), content="\n".join(H),
title=page_title, 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), 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( def get_sorted_etuds_notes(
evaluation: Evaluation, etudids: list, formsemestre_id: int evaluation: Evaluation, etudids: list, formsemestre_id: int
) -> list[dict]: ) -> list[dict]:
...@@ -815,13 +1013,13 @@ def get_sorted_etuds_notes( ...@@ -815,13 +1013,13 @@ def get_sorted_etuds_notes(
if notes_db[etudid]["uid"] if notes_db[etudid]["uid"]
else None else None
) )
e["explanation"] = ( e[
f"""{ "explanation"
] = f"""{
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M") notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M")
} par {user.get_nomplogin() if user else '?' } par {user.get_nomplogin() if user else '?'
} {(' : ' + notes_db[etudid]["comment"]) if notes_db[etudid]["comment"] else ''} } {(' : ' + notes_db[etudid]["comment"]) if notes_db[etudid]["comment"] else ''}
""" """
)
else: else:
e["val"] = "" e["val"] = ""
e["explanation"] = "" e["explanation"] = ""
...@@ -947,7 +1145,7 @@ def _form_saisie_notes( ...@@ -947,7 +1145,7 @@ def _form_saisie_notes(
explanation = ( explanation = (
"" ""
if disabled if disabled
else f"""<span id="hist_{etudid}">{ else f"""<span id="formnotes_hist_{etudid}">{
get_note_history_menu(evaluation.id, etudid) get_note_history_menu(evaluation.id, etudid)
}</span>""" }</span>"""
) )
...@@ -1061,6 +1259,7 @@ def save_notes( ...@@ -1061,6 +1259,7 @@ def save_notes(
return json_error(403, "modification notes non autorisee pour cet utilisateur") return json_error(403, "modification notes non autorisee pour cet utilisateur")
# #
valid_notes, _, _, _, _ = check_notes(notes, evaluation) valid_notes, _, _, _, _ = check_notes(notes, evaluation)
if valid_notes: if valid_notes:
etudids_changed, _, etudids_with_decision, messages = notes_add( etudids_changed, _, etudids_with_decision, messages = notes_add(
current_user, evaluation.id, valid_notes, comment=comment, do_it=True current_user, evaluation.id, valid_notes, comment=comment, do_it=True
...@@ -1120,7 +1319,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str: ...@@ -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 nv = "" # ne repete pas la valeur de la note courante
else: else:
# ancienne valeur # ancienne valeur
nv = f": {dispnote}" nv = f": {scu.fmt_note(dispnote)}"
first = False first = False
if i["comment"]: if i["comment"]:
comment = f' <span class="histcomment">{i["comment"]}</span>' comment = f' <span class="histcomment">{i["comment"]}</span>'
......
// 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 let nbSaving = 0; // nombre de requêtes en cours
...@@ -24,52 +64,41 @@ function decSaving() { ...@@ -24,52 +64,41 @@ function decSaving() {
} }
} }
document.addEventListener("DOMContentLoaded", function () { window.addEventListener('beforeunload', function (e) {
let noteInputs = document.querySelectorAll("#formnotes .note"); const noteInputs = document.querySelectorAll("#formnotes .note");
noteInputs.forEach(function (input) { noteInputs.forEach(function (input) {
input.addEventListener("input", function() { if (input.getAttribute("data-modified") === "true" && input.value !== input.getAttribute("data-last-saved-value")) {
this.setAttribute("data-modified", "true"); valid_note.call(input);
});
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);
});
var masquerBtn = document.querySelector(".btn_masquer_DEM");
masquerBtn.addEventListener("click", masquer_DEM);
});
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
} }
});
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 is_valid_note(v) { function is_valid_note(field_id, v) {
if (!v) return true; 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]*$")) { if (!v.match("^-?[0-9]*.?[0-9]*$")) {
return v == "ABS" || v == "EXC" || v == "SUPR" || v == "ATT" || v == "DEM"; return v == "ABS" || v == "EXC" || v == "SUPR" || v == "ATT" || v == "DEM";
} else { } else {
var x = parseFloat(v); const x = parseFloat(v);
return x >= note_min && x <= note_max; return x >= get_note_min(field_id) && x <= get_note_max(field_id);
} }
} }
function valid_note(e) { function valid_note() {
var v = this.value.trim().toUpperCase().replace(",", "."); const field_id = get_field_id(this)
if (is_valid_note(v)) { const v = this.value.trim().toUpperCase().replace(",", ".");
if (is_valid_note(field_id, v)) {
if (v && v != this.getAttribute("data-last-saved-value")) { if (v && v != this.getAttribute("data-last-saved-value")) {
this.className = "note_valid_new"; this.className = "note_valid_new";
const etudid = parseInt(this.getAttribute("data-etudid")); const etudid = get_etudid(field_id);
save_note(this, v, etudid); save_note(field_id, v);
} }
} else { } else {
/* Saisie invalide */ /* Saisie invalide */
...@@ -78,10 +107,33 @@ function valid_note(e) { ...@@ -78,10 +107,33 @@ function valid_note(e) {
} }
} }
async function save_note(elem, v, etudid) { function get_field_id(elt) { // la forme des id est ????_#### (où #### est le field_id que l'on cherche)
let evaluation_id = document.querySelector("#formnotes_evaluation_id").getAttribute("value"); return elt.id.split('_').pop(); // formnotes_note_#### ou eval_####
let formsemestre_id = document.querySelector("#formnotes_formsemestre_id").getAttribute("value"); }
var scoMsg = document.getElementById("sco_msg");
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.innerHTML = "en cours...";
scoMsg.style.display = "block"; scoMsg.style.display = "block";
incSaving(); // update counter to show one more saving in progress incSaving(); // update counter to show one more saving in progress
...@@ -103,106 +155,51 @@ async function save_note(elem, v, etudid) { ...@@ -103,106 +155,51 @@ async function save_note(elem, v, etudid) {
sco_message("Erreur: valeur non enregistrée"); sco_message("Erreur: valeur non enregistrée");
} else { } else {
const data = await response.json(); const data = await response.json();
var scoMsg = document.getElementById("sco_msg");
scoMsg.style.display = "none"; scoMsg.style.display = "none";
if (data.etudids_changed.length > 0) { if (data.etudids_changed.length > 0) {
sco_message("enregistré"); sco_message("enregistré");
elem.className = "note_saved"; note_field.className = "note_saved";
// Il y avait une decision de jury ? // Il y avait une decision de jury ?
if (data.etudids_with_decision.includes(etudid)) { if (data.etudids_with_decision.includes(etudid)) {
if (v !== elem.getAttribute("data-orig-value")) { update_jurylink(field_id, v);
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 = "";
}
} }
// Mise à jour menu historique // Mise à jour menu historique
if (data.history_menu[etudid]) { if (data.history_menu[etudid]) {
var historyElem = document.getElementById("hist_" + etudid); hist_field.innerHTML = data.history_menu[etudid];
historyElem.innerHTML = data.history_menu[etudid];
} }
elem.setAttribute("data-last-saved-value", v); note_field.setAttribute("data-last-saved-value", v);
} }
} }
} catch (error) { } catch (error) {
console.error("Fetch error:", error); console.error("Fetch error:", error);
sco_message("Erreur réseau: valeur non enregistrée"); sco_message("Erreur réseau: valeur non enregistrée");
} finally { } 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 function make_jurylink(field_id) {
window.addEventListener('beforeunload', function (e) { const formsemestre_id = get_formsemestre_id();
let noteInputs = document.querySelectorAll("#formnotes .note"); const etudid = get_etudid(field_id);
noteInputs.forEach(function (input) { const href = `${SCO_URL}Notes/formsemestre_validation_etud_form?formsemestre_id=${formsemestre_id}&etudid=${etudid}`;
if (input.getAttribute("data-modified") === "true" && input.value !== input.getAttribute("data-last-saved-value")) { return '<a href="' + href +'">mettre à jour décision de jury</a>';
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) { function change_history(e) {
let opt = e.selectedOptions[0]; const opt = e.selectedOptions[0];
let val = opt.getAttribute("data-note"); const val = opt.getAttribute("data-note");
const etudid = parseInt(e.getAttribute("data-etudid")); if (val != '') {
const field_id = get_field_id(e.parentElement);
// le input associé a ce menu: // le input associé a ce menu:
let input_elem = e.parentElement.parentElement.parentElement.childNodes[0]; const input = get_note_field(field_id)
input_elem.value = val; save_note(field_id, val);
save_note(input_elem, val, etudid); 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")) {
// Contribution S.L.: copier/coller des notes input.classList.remove("note_saved")
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 { } else {
return; input.classList.add("note_saved");
} }
} }
} }
function masquer_DEM() {
document.querySelector("body").classList.toggle("masquer_DEM");
}
/*
* 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
// 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
{% 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 %}
...@@ -1816,6 +1816,22 @@ def form_saisie_notes(evaluation_id: int): ...@@ -1816,6 +1816,22 @@ def form_saisie_notes(evaluation_id: int):
return sco_saisie_notes.saisie_notes(evaluation, group_ids) 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"]) @bp.route("/formsemestre_import_notes/<int:formsemestre_id>", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.ScoView) # controle contextuel @permission_required(Permission.ScoView) # controle contextuel
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment