Skip to content
Snippets Groups Projects
Commit 183f1fe3 authored by Jean-Marie Place's avatar Jean-Marie Place
Browse files

resolution t1030

parent 0f2b0dc8
Branches
No related tags found
No related merge requests found
...@@ -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 == "I" 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": "Gérer 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
...@@ -111,6 +112,26 @@ def convert_note_from_string( ...@@ -111,6 +112,26 @@ def convert_note_from_string(
return note_value, invalid return note_value, invalid
def get_bounds(evaluation: Evaluation) -> tuple[int, int]:
"""Calcule les notes min et max pour une évaluation."""
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
return note_min, note_max
def check_notes( def check_notes(
notes: list[(int, float | str)], evaluation: Evaluation notes: list[(int, float | str)], evaluation: Evaluation
) -> tuple[list[tuple[int, float]], list[int], list[int], list[int], list[int]]: ) -> tuple[list[tuple[int, float]], list[int], list[int], list[int], list[int]]:
...@@ -128,24 +149,10 @@ def check_notes( ...@@ -128,24 +149,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 = get_bounds(evaluation)
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 +462,11 @@ def notes_add( ...@@ -455,11 +462,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 +769,162 @@ def saisie_notes(evaluation: Evaluation, group_ids: list[int] | tuple[int] = ()) ...@@ -762,11 +769,162 @@ 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):
self.name = scu.MODULE_TYPE_NAMES[module_type]
self.data_modimpl = []
def add_modimpl(self, modimpl):
self.data_modimpl.append(modimpl)
class DataModimpl:
"""Aggrèges les données sur un module pour un étudiants donné.
# 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: lal iste des évaluations du module.
"""
def __init__(self, etudiant: Identite, modimpl):
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):
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, evaluation):
self.etudid = etudid
self.evaluation = evaluation
self.evaluation_id = evaluation.id
self.note_min, self.note_max = get_bounds(evaluation)
self.data_debut = evaluation.date_fin
self.date_fin = evaluation.date_fin
self.description = evaluation.description
self.ue_poids = evaluation.get_ue_poids_dict()
self.evaluation_type = evaluation.evaluation_type
self.note = NotesNotes.query.filter_by(
evaluation_id=self.evaluation.id, etudid=self.etudid
).first()
self.note_value = "" if self.note is None else self.note.value
self.history = f"""<span id="formnotes_hist_{self.evaluation_id}">{
get_note_history_menu(evaluation.id, etudid)
}</span>"""
def forminput(self):
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 get_data(semestre: FormSemestre, etudiant: Identite) -> dict:
"""Récupère les infos pour un étudiant (cf ticket 1030)"""
res = res_sem.load_formsemestre_results(semestre)
data = {
"decision_jury": res.etud_has_decision(etudiant.etudid, include_rcues=False),
"data_sections": {},
}
if semestre.formation.is_apc():
for module_type in ModuleType:
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 ValueError("Tentative d accès à une inscription inexistante") # bug
if ins.etat != "I":
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)
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),
semestre=semestre,
etudiant=etudiant,
data=data,
readonly=readonly,
get_note_history_menu=get_note_history_menu,
module_type_order=[
ModuleType.RESSOURCE,
ModuleType.SAE,
ModuleType.STANDARD,
ModuleType.MALUS,
],
)
# US 1030 - FIN
def get_evaluation_etud_note(evaluation: Evaluation, etudid: int):
a = Notes.query.filter_by(id=evaluation.id, etudid=etudid).first()
pass
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 +973,13 @@ def get_sorted_etuds_notes( ...@@ -815,13 +973,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 +1105,7 @@ def _form_saisie_notes( ...@@ -947,7 +1105,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 +1219,7 @@ def save_notes( ...@@ -1061,6 +1219,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 +1279,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str: ...@@ -1120,7 +1279,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,23 @@ function valid_note(e) { ...@@ -78,10 +107,23 @@ 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 write_on_blur(elt) {
if (elt.getAttribute("data-modified") === "true") {
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 +145,51 @@ async function save_note(elem, v, etudid) { ...@@ -103,106 +145,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_note_field(field_id) {
return document.getElementById(note_prefix + field_id);
}
function get_hist_field(field_id) {
return document.getElementById(hist_prefix + field_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_note_field(field_id) {
return document.getElementById(note_prefix + field_id);
}
function get_hist_field(field_id) {
return document.getElementById(hist_prefix + field_id);
}
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() {
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)} ));
});
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 : {{etudiant.nom}} {{etudiant.prenom}} </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="{{etudiant.id}}" />
<input type="hidden" id="formnotes_formsemestre_id" value="{{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.description }}
<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 }}">
<input type="hidden" id="evaluation_type_{{ data_eval.evaluation_id }}" value="{{ data_eval.evaluation_type }}">
{% 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 to comment