Compare commits

..

No commits in common. "72b0ed17b5b36550b5343d27e30e8b03ff206503" and "4b4e52bf2dba250d589909a7781c33b3c88fc4b8" have entirely different histories.

14 changed files with 299 additions and 367 deletions

View File

@ -8,7 +8,7 @@
ScoDoc 9 API : accès aux évaluations ScoDoc 9 API : accès aux évaluations
""" """
from flask import g, request from flask import g
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import login_required
@ -17,7 +17,7 @@ import app
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import Evaluation, ModuleImpl, FormSemestre from app.models import Evaluation, ModuleImpl, FormSemestre
from app.scodoc import sco_evaluation_db, sco_saisie_notes from app.scodoc import sco_evaluation_db
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -28,7 +28,7 @@ import app.scodoc.sco_utils as scu
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def evaluation(evaluation_id: int): def the_eval(evaluation_id: int):
"""Description d'une évaluation. """Description d'une évaluation.
{ {
@ -93,22 +93,24 @@ def evaluations(moduleimpl_id: int):
@as_json @as_json
def evaluation_notes(evaluation_id: int): def evaluation_notes(evaluation_id: int):
""" """
Retourne la liste des notes de l'évaluation Retourne la liste des notes à partir de l'id d'une évaluation donnée
evaluation_id : l'id de l'évaluation evaluation_id : l'id d'une évaluation
Exemple de résultat : Exemple de résultat :
{ {
"11": { "1": {
"etudid": 11, "id": 1,
"etudid": 10,
"evaluation_id": 1, "evaluation_id": 1,
"value": 15.0, "value": 15.0,
"comment": "", "comment": "",
"date": "Wed, 20 Apr 2022 06:49:05 GMT", "date": "Wed, 20 Apr 2022 06:49:05 GMT",
"uid": 2 "uid": 2
}, },
"12": { "2": {
"etudid": 12, "id": 2,
"etudid": 1,
"evaluation_id": 1, "evaluation_id": 1,
"value": 12.0, "value": 12.0,
"comment": "", "comment": "",
@ -126,8 +128,8 @@ def evaluation_notes(evaluation_id: int):
.filter_by(dept_id=g.scodoc_dept_id) .filter_by(dept_id=g.scodoc_dept_id)
) )
evaluation = query.first_or_404() the_eval = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement dept = the_eval.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
@ -135,49 +137,7 @@ def evaluation_notes(evaluation_id: int):
# "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval. # "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval.
note = notes[etudid] note = notes[etudid]
note["value"] = scu.fmt_note(note["value"], keep_numeric=True) note["value"] = scu.fmt_note(note["value"], keep_numeric=True)
note["note_max"] = evaluation.note_max note["note_max"] = the_eval.note_max
del note["id"] del note["id"]
# in JS, keys must be string, not integers return notes
return {str(etudid): note for etudid, note in notes.items()}
@bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
@api_web_bp.route("/evaluation/<int:evaluation_id>/notes/set", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEnsView)
@as_json
def evaluation_set_notes(evaluation_id: int):
"""Écriture de notes dans une évaluation.
The request content type should be "application/json",
and contains:
{
'notes' : [ (etudid, value), ... ],
'comment' : opetional string
}
Result:
- nb_changed: nombre de notes changées
- nb_suppress: nombre de notes effacées
- etudids_with_decision: liste des etudiants dont la note a changé
alors qu'ils ont une décision de jury enregistrée.
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
evaluation = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym)
data = request.get_json(force=True) # may raise 400 Bad Request
notes = data.get("notes")
if notes is None:
return scu.json_error(404, "no notes")
if not isinstance(notes, list):
return scu.json_error(404, "invalid notes argument (must be a list)")
return sco_saisie_notes.save_notes(
evaluation, notes, comment=data.get("comment", "")
)

View File

@ -55,7 +55,7 @@ class Module(db.Model):
secondary=parcours_modules, secondary=parcours_modules,
lazy="subquery", lazy="subquery",
backref=db.backref("modules", lazy=True), backref=db.backref("modules", lazy=True),
order_by="ApcParcours.numero, ApcParcours.code", order_by="ApcParcours.numero",
) )
app_critiques = db.relationship( app_critiques = db.relationship(

View File

@ -58,10 +58,7 @@ class UniteEns(db.Model):
# Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble # Une UE appartient soit à tous les parcours (tronc commun), soit à un sous-ensemble
parcours = db.relationship( parcours = db.relationship(
ApcParcours, ApcParcours, secondary="ue_parcours", backref=db.backref("ues", lazy=True)
secondary="ue_parcours",
backref=db.backref("ues", lazy=True),
order_by="ApcParcours.numero, ApcParcours.code",
) )
# relations # relations

View File

@ -252,7 +252,7 @@ def do_evaluation_delete(evaluation_id):
def do_evaluation_get_all_notes( def do_evaluation_get_all_notes(
evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None
): ):
"""Toutes les notes pour une évaluation: { etudid : { 'value' : value, 'date' : date ... }} """Toutes les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }}
Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module. Attention: inclut aussi les notes des étudiants qui ne sont plus inscrits au module.
""" """
# pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant

View File

@ -594,7 +594,6 @@ def formsemestre_description_table(
formsemestre: FormSemestre = FormSemestre.query.filter_by( formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404() ).first_or_404()
is_apc = formsemestre.formation.is_apc()
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours) parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours)
@ -608,7 +607,7 @@ def formsemestre_description_table(
else: else:
ues = formsemestre.get_ues() ues = formsemestre.get_ues()
columns_ids += [f"ue_{ue.id}" for ue in ues] columns_ids += [f"ue_{ue.id}" for ue in ues]
if sco_preferences.get_preference("bul_show_ects", formsemestre_id) and not is_apc: if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
columns_ids += ["ects"] columns_ids += ["ects"]
columns_ids += ["Inscrits", "Responsable", "Enseignants"] columns_ids += ["Inscrits", "Responsable", "Enseignants"]
if with_evals: if with_evals:
@ -635,7 +634,6 @@ def formsemestre_description_table(
sum_coef = 0 sum_coef = 0
sum_ects = 0 sum_ects = 0
last_ue_id = None last_ue_id = None
formsemestre_parcours_ids = {p.id for p in formsemestre.parcours}
for modimpl in formsemestre.modimpls_sorted: for modimpl in formsemestre.modimpls_sorted:
# Ligne UE avec ECTS: # Ligne UE avec ECTS:
ue = modimpl.module.ue ue = modimpl.module.ue
@ -662,7 +660,7 @@ def formsemestre_description_table(
ue_info[ ue_info[
f"_{k}_td_attrs" f"_{k}_td_attrs"
] = f'style="background-color: {ue.color} !important;"' ] = f'style="background-color: {ue.color} !important;"'
if not is_apc: if not formsemestre.formation.is_apc():
# n'affiche la ligne UE qu'en formation classique # n'affiche la ligne UE qu'en formation classique
# car l'UE de rattachement n'a pas d'intérêt en BUT # car l'UE de rattachement n'a pas d'intérêt en BUT
rows.append(ue_info) rows.append(ue_info)
@ -703,17 +701,8 @@ def formsemestre_description_table(
for ue in ues: for ue in ues:
row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or "" row[f"ue_{ue.id}"] = coef_dict.get(ue.id, 0.0) or ""
if with_parcours: if with_parcours:
# Intersection des parcours du module avec ceux du formsemestre
row["parcours"] = ", ".join( row["parcours"] = ", ".join(
[ sorted([pa.code for pa in modimpl.module.parcours])
pa.code
for pa in (
modimpl.module.parcours
if modimpl.module.parcours
else modimpl.formsemestre.parcours
)
if pa.id in formsemestre_parcours_ids
]
) )
rows.append(row) rows.append(row)

View File

@ -36,20 +36,16 @@ import flask
from flask import g, url_for, request from flask import g, url_for, request
from flask_login import current_user from flask_login import current_user
from app import log
from app.auth.models import User from app.auth.models import User
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import ( from app.models import Evaluation, FormSemestre
Evaluation, from app.models import ModuleImpl, NotesNotes, ScolarNews
FormSemestre,
Module,
ModuleImpl,
NotesNotes,
ScolarNews,
)
from app.models.etudiants import Identite from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
AccessDenied, AccessDenied,
InvalidNoteValue, InvalidNoteValue,
@ -59,14 +55,14 @@ from app.scodoc.sco_exceptions import (
ScoInvalidParamError, ScoInvalidParamError,
ScoValueError, ScoValueError,
) )
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc import html_sco_header, sco_users from app.scodoc import html_sco_header, sco_users
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
@ -74,11 +70,7 @@ from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes from app.scodoc import sco_undo_notes
import app.scodoc.notesdb as ndb from app.scodoc import sco_etud
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import json_error
from app.scodoc.sco_utils import ModuleType
def convert_note_from_string( def convert_note_from_string(
@ -136,30 +128,29 @@ def _displayNote(val):
return val return val
def _check_notes(notes: list[(int, float)], evaluation: Evaluation): def _check_notes(notes: list[(int, float)], evaluation: dict, mod: dict):
# XXX typehint : float or str # XXX typehint : float or str
"""notes is a list of tuples (etudid, value) """notes is a list of tuples (etudid, value)
mod is the module (used to ckeck type, for malus) mod is the module (used to ckeck type, for malus)
returns list of valid notes (etudid, float value) returns list of valid notes (etudid, float value)
and 4 lists of etudid: etudids_invalids, etudids_without_notes, etudids_absents, etudid_to_suppress and 4 lists of etudid: invalids, withoutnotes, absents, tosuppress, existingjury
""" """
note_max = evaluation.note_max or 0.0 note_max = evaluation["note_max"]
module: Module = evaluation.moduleimpl.module if mod["module_type"] in (
if module.module_type in (
scu.ModuleType.STANDARD, scu.ModuleType.STANDARD,
scu.ModuleType.RESSOURCE, scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE, scu.ModuleType.SAE,
): ):
note_min = scu.NOTES_MIN note_min = scu.NOTES_MIN
elif module.module_type == ModuleType.MALUS: elif mod["module_type"] == ModuleType.MALUS:
note_min = -20.0 note_min = -20.0
else: else:
raise ValueError("Invalid module type") # bug raise ValueError("Invalid module type") # bug
valid_notes = [] # liste (etudid, note) des notes ok (ou absent) L = [] # liste (etudid, note) des notes ok (ou absent)
etudids_invalids = [] # etudid avec notes invalides invalids = [] # etudid avec notes invalides
etudids_without_notes = [] # etudid sans notes (champs vides) withoutnotes = [] # etudid sans notes (champs vides)
etudids_absents = [] # etudid absents absents = [] # etudid absents
etudid_to_suppress = [] # etudids avec ancienne note à supprimer tosuppress = [] # etudids avec ancienne note à supprimer
for etudid, note in notes: for etudid, note in notes:
note = str(note).strip().upper() note = str(note).strip().upper()
@ -175,34 +166,31 @@ def _check_notes(notes: list[(int, float)], evaluation: Evaluation):
note_max, note_max,
note_min=note_min, note_min=note_min,
etudid=etudid, etudid=etudid,
absents=etudids_absents, absents=absents,
tosuppress=etudid_to_suppress, tosuppress=tosuppress,
invalids=etudids_invalids, invalids=invalids,
) )
if not invalid: if not invalid:
valid_notes.append((etudid, value)) L.append((etudid, value))
else: else:
etudids_without_notes.append(etudid) withoutnotes.append(etudid)
return ( return L, invalids, withoutnotes, absents, tosuppress
valid_notes,
etudids_invalids,
etudids_without_notes,
etudids_absents,
etudid_to_suppress,
)
def do_evaluation_upload_xls(): def do_evaluation_upload_xls():
""" """
Soumission d'un fichier XLS (evaluation_id, notefile) Soumission d'un fichier XLS (evaluation_id, notefile)
""" """
authuser = current_user
vals = scu.get_request_args() vals = scu.get_request_args()
evaluation_id = int(vals["evaluation_id"]) evaluation_id = int(vals["evaluation_id"])
comment = vals["comment"] comment = vals["comment"]
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
# Check access (admin, respformation, and responsable_id) M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id): # Check access
raise AccessDenied(f"Modification des notes impossible pour {current_user}") # (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]):
raise AccessDenied("Modification des notes impossible pour %s" % authuser)
# #
diag, lines = sco_excel.excel_file_to_list(vals["notefile"]) diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
try: try:
@ -251,16 +239,14 @@ def do_evaluation_upload_xls():
if etudid: if etudid:
notes.append((etudid, val)) notes.append((etudid, val))
ni += 1 ni += 1
except Exception as exc: except:
diag.append( diag.append(
f"""Erreur: Ligne invalide ! (erreur ligne {ni})<br>{lines[ni]}""" f"""Erreur: Ligne invalide ! (erreur ligne {ni})<br>{lines[ni]}"""
) )
raise InvalidNoteValue() from exc raise InvalidNoteValue()
# -- check values # -- check values
valid_notes, invalids, withoutnotes, absents, _ = _check_notes( L, invalids, withoutnotes, absents, _ = _check_notes(notes, E, M["module"])
notes, evaluation if len(invalids):
)
if invalids:
diag.append( diag.append(
f"Erreur: la feuille contient {len(invalids)} notes invalides</p>" f"Erreur: la feuille contient {len(invalids)} notes invalides</p>"
) )
@ -272,33 +258,37 @@ def do_evaluation_upload_xls():
diag.append("Notes invalides pour: " + ", ".join(etudsnames)) diag.append("Notes invalides pour: " + ", ".join(etudsnames))
raise InvalidNoteValue() raise InvalidNoteValue()
else: else:
etudids_changed, nb_suppress, etudids_with_decisions = notes_add( nb_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, valid_notes, comment authuser, evaluation_id, L, comment
) )
# news # news
module: Module = evaluation.moduleimpl.module E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[
status_url = url_for( 0
]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=evaluation.moduleimpl_id, moduleimpl_id=mod["moduleimpl_id"],
_external=True, _external=True,
) )
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
obj=evaluation.moduleimpl_id, obj=M["moduleimpl_id"],
text=f"""Chargement notes dans <a href="{status_url}">{ text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
module.titre or module.code}</a>""", url=mod["url"],
url=status_url,
max_frequency=30 * 60, # 30 minutes max_frequency=30 * 60, # 30 minutes
) )
msg = f"""<p>{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, { msg = (
len(absents)} absents, {nb_suppress} note supprimées) "<p>%d notes changées (%d sans notes, %d absents, %d note supprimées)</p>"
</p>""" % (nb_changed, len(withoutnotes), len(absents), nb_suppress)
if etudids_with_decisions: )
msg += """<p class="warning">Important: il y avait déjà des décisions de jury if existing_decisions:
enregistrées, qui sont peut-être à revoir suite à cette modification !</p> msg += """<p class="warning">Important: il y avait déjà des décisions de jury enregistrées, qui sont potentiellement à revoir suite à cette modification !</p>"""
""" # msg += '<p>' + str(notes) # debug
return 1, msg return 1, msg
except InvalidNoteValue: except InvalidNoteValue:
@ -320,12 +310,14 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id): if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id):
raise AccessDenied(f"Modification des notes impossible pour {current_user}") raise AccessDenied(f"Modification des notes impossible pour {current_user}")
# Convert and check value # Convert and check value
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation) L, invalids, _, _, _ = _check_notes(
[(etud.id, value)], evaluation.to_dict(), evaluation.moduleimpl.module.to_dict()
)
if len(invalids) == 0: if len(invalids) == 0:
etudids_changed, _, _ = notes_add( nb_changed, _, _ = notes_add(
current_user, evaluation.id, L, "Initialisation notes" current_user, evaluation.id, L, "Initialisation notes"
) )
if len(etudids_changed) == 1: if nb_changed == 1:
return True return True
return False # error return False # error
@ -360,7 +352,9 @@ def do_evaluation_set_missing(
if etudid not in notes_db: # pas de note if etudid not in notes_db: # pas de note
notes.append((etudid, value)) notes.append((etudid, value))
# Convert and check values # Convert and check values
valid_notes, invalids, _, _, _ = _check_notes(notes, evaluation) L, invalids, _, _, _ = _check_notes(
notes, evaluation.to_dict(), modimpl.module.to_dict()
)
dest_url = url_for( dest_url = url_for(
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id
) )
@ -378,13 +372,13 @@ def do_evaluation_set_missing(
""" """
# Confirm action # Confirm action
if not dialog_confirmed: if not dialog_confirmed:
plural = len(valid_notes) > 1 plural = len(L) > 1
return scu.confirm_dialog( return scu.confirm_dialog(
f"""<h2>Mettre toutes les notes manquantes de l'évaluation f"""<h2>Mettre toutes les notes manquantes de l'évaluation
à la valeur {value} ?</h2> à la valeur {value} ?</h2>
<p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC) <p>Seuls les étudiants pour lesquels aucune note (ni valeur, ni ABS, ni EXC)
n'a été rentrée seront affectés.</p> n'a été rentrée seront affectés.</p>
<p><b>{len(valid_notes)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""} <p><b>{len(L)} étudiant{"s" if plural else ""} concerné{"s" if plural else ""}
par ce changement de note.</b> par ce changement de note.</b>
</p> </p>
""", """,
@ -398,7 +392,7 @@ def do_evaluation_set_missing(
) )
# ok # ok
comment = "Initialisation notes manquantes" comment = "Initialisation notes manquantes"
etudids_changed, _, _ = notes_add(current_user, evaluation_id, valid_notes, comment) nb_changed, _, _ = notes_add(current_user, evaluation_id, L, comment)
# news # news
url = url_for( url = url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
@ -414,7 +408,7 @@ def do_evaluation_set_missing(
) )
return f""" return f"""
{ html_sco_header.sco_header() } { html_sco_header.sco_header() }
<h2>{len(etudids_changed)} notes changées</h2> <h2>{nb_changed} notes changées</h2>
<ul> <ul>
<li><a class="stdlink" href="{dest_url}"> <li><a class="stdlink" href="{dest_url}">
Revenir au formulaire de saisie des notes</a> Revenir au formulaire de saisie des notes</a>
@ -460,7 +454,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
) )
if not dialog_confirmed: if not dialog_confirmed:
etudids_changed, nb_suppress, existing_decisions = notes_add( nb_changed, nb_suppress, existing_decisions = notes_add(
current_user, evaluation_id, notes, do_it=False, check_inscription=False current_user, evaluation_id, notes, do_it=False, check_inscription=False
) )
msg = f"""<p>Confirmer la suppression des {nb_suppress} notes ? msg = f"""<p>Confirmer la suppression des {nb_suppress} notes ?
@ -481,14 +475,14 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
) )
# modif # modif
etudids_changed, nb_suppress, existing_decisions = notes_add( nb_changed, nb_suppress, existing_decisions = notes_add(
current_user, current_user,
evaluation_id, evaluation_id,
notes, notes,
comment="effacer tout", comment="effacer tout",
check_inscription=False, check_inscription=False,
) )
assert len(etudids_changed) == nb_suppress assert nb_changed == nb_suppress
H = [f"""<p>{nb_suppress} notes supprimées</p>"""] H = [f"""<p>{nb_suppress} notes supprimées</p>"""]
if existing_decisions: if existing_decisions:
H.append( H.append(
@ -522,7 +516,7 @@ def notes_add(
comment=None, comment=None,
do_it=True, do_it=True,
check_inscription=True, check_inscription=True,
) -> tuple[list[int], int, list[int]]: ) -> tuple:
""" """
Insert or update notes Insert or update notes
notes is a list of tuples (etudid,value) notes is a list of tuples (etudid,value)
@ -530,12 +524,11 @@ def notes_add(
WOULD be changed or suppressed. WOULD be changed or suppressed.
Nota: Nota:
- si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log) - si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log)
Return tuple (nb_changed, nb_suppress, existing_decisions)
Return: tuple (etudids_changed, nb_suppress, etudids_with_decision)
""" """
now = psycopg2.Timestamp(*time.localtime()[:6]) now = psycopg2.Timestamp(*time.localtime()[:6])
# Vérifie inscription et valeur note # Verifie inscription et valeur note
inscrits = { inscrits = {
x[0] x[0]
for x in sco_groups.do_evaluation_listeetuds_groups( for x in sco_groups.do_evaluation_listeetuds_groups(
@ -554,13 +547,13 @@ def notes_add(
# Met a jour la base # Met a jour la base
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
etudids_changed = [] nb_changed = 0
nb_suppress = 0 nb_suppress = 0
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre formsemestre: FormSemestre = evaluation.moduleimpl.formsemestre
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# etudids pour lesquels il y a une decision de jury et que la note change: # etudids pour lesquels il y a une decision de jury et que la note change:
etudids_with_decision = [] etudids_with_existing_decision = []
try: try:
for etudid, value in notes: for etudid, value in notes:
changed = False changed = False
@ -568,7 +561,7 @@ def notes_add(
# nouvelle note # nouvelle note
if value != scu.NOTES_SUPPRESS: if value != scu.NOTES_SUPPRESS:
if do_it: if do_it:
args = { aa = {
"etudid": etudid, "etudid": etudid,
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
"value": value, "value": value,
@ -576,21 +569,27 @@ def notes_add(
"uid": user.id, "uid": user.id,
"date": now, "date": now,
} }
ndb.quote_dict(args) ndb.quote_dict(aa)
# Note: le conflit ci-dessous peut arriver si un autre thread try:
# a modifié la base après qu'on ait lu notes_db
cursor.execute( cursor.execute(
"""INSERT INTO notes_notes """INSERT INTO notes_notes
(etudid, evaluation_id, value, comment, date, uid) (etudid, evaluation_id, value, comment, date, uid)
VALUES VALUES (%(etudid)s,%(evaluation_id)s,%(value)s,%(comment)s,%(date)s,%(uid)s)
(%(etudid)s,%(evaluation_id)s,%(value)s,
%(comment)s,%(date)s,%(uid)s)
ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key
DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s,
value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s
""", """,
args, aa,
) )
except psycopg2.errors.UniqueViolation as exc:
# XXX ne devrait pas arriver mais bug possible ici (non reproductible)
existing_note = NotesNotes.query.filter_by(
evaluation_id=evaluation_id, etudid=etudid
).first()
sco_cache.EvaluationCache.delete(evaluation_id)
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
evaluation_id
)
raise ScoBugCatcher(
f"dup: existing={existing_note} etudid={repr(etudid)} value={value} in_db={etudid in notes_db}"
) from exc
changed = True changed = True
else: else:
# il y a deja une note # il y a deja une note
@ -616,7 +615,7 @@ def notes_add(
""", """,
{"etudid": etudid, "evaluation_id": evaluation_id}, {"etudid": etudid, "evaluation_id": evaluation_id},
) )
args = { aa = {
"etudid": etudid, "etudid": etudid,
"evaluation_id": evaluation_id, "evaluation_id": evaluation_id,
"value": value, "value": value,
@ -624,7 +623,7 @@ def notes_add(
"comment": comment, "comment": comment,
"uid": user.id, "uid": user.id,
} }
ndb.quote_dict(args) ndb.quote_dict(aa)
if value != scu.NOTES_SUPPRESS: if value != scu.NOTES_SUPPRESS:
if do_it: if do_it:
cursor.execute( cursor.execute(
@ -633,36 +632,34 @@ def notes_add(
WHERE etudid = %(etudid)s WHERE etudid = %(etudid)s
and evaluation_id = %(evaluation_id)s and evaluation_id = %(evaluation_id)s
""", """,
args, aa,
) )
else: # suppression ancienne note else: # suppression ancienne note
if do_it: if do_it:
log( log(
f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={ "notes_add, suppress, evaluation_id=%s, etudid=%s, oldval=%s"
etudid}, oldval={oldval}""" % (evaluation_id, etudid, oldval)
) )
cursor.execute( cursor.execute(
"""DELETE FROM notes_notes """DELETE FROM notes_notes
WHERE etudid = %(etudid)s WHERE etudid = %(etudid)s
AND evaluation_id = %(evaluation_id)s AND evaluation_id = %(evaluation_id)s
""", """,
args, aa,
) )
# garde trace de la suppression dans l'historique: # garde trace de la suppression dans l'historique:
args["value"] = scu.NOTES_SUPPRESS aa["value"] = scu.NOTES_SUPPRESS
cursor.execute( cursor.execute(
"""INSERT INTO notes_notes_log """INSERT INTO notes_notes_log (etudid,evaluation_id,value,comment,date,uid)
(etudid,evaluation_id,value,comment,date,uid) VALUES (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
VALUES
(%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s)
""", """,
args, aa,
) )
nb_suppress += 1 nb_suppress += 1
if changed: if changed:
etudids_changed.append(etudid) nb_changed += 1
if res.etud_has_decision(etudid): if res.etud_has_decision(etudid):
etudids_with_decision.append(etudid) etudids_with_existing_decision.append(etudid)
except Exception as exc: except Exception as exc:
log("*** exception in notes_add") log("*** exception in notes_add")
if do_it: if do_it:
@ -675,7 +672,7 @@ def notes_add(
cnx.commit() cnx.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
sco_cache.EvaluationCache.delete(evaluation_id) sco_cache.EvaluationCache.delete(evaluation_id)
return etudids_changed, nb_suppress, etudids_with_decision return nb_changed, nb_suppress, etudids_with_existing_decision
def saisie_notes_tableur(evaluation_id, group_ids=()): def saisie_notes_tableur(evaluation_id, group_ids=()):
@ -1348,56 +1345,48 @@ def _form_saisie_notes(
return None return None
def save_notes( def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
evaluation: Evaluation, notes: list[tuple[(int, float)]], comment: str = "" """Enregistre une note (ajax)"""
) -> dict: log(
"""Enregistre une liste de notes. f"save_note: evaluation_id={evaluation_id} etudid={etudid} uid={current_user} value={value}"
Vérifie que les étudiants sont bien inscrits à ce module, et que l'on a le droit. )
Result: dict avec E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
""" M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
log(f"save_note: evaluation_id={evaluation.id} uid={current_user} notes={notes}") Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
status_url = url_for( Mod["url"] = url_for(
"notes.moduleimpl_status", "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=evaluation.moduleimpl_id, moduleimpl_id=M["moduleimpl_id"],
_external=True, _external=True,
) )
result = {"nbchanged": 0} # JSON
# Check access: admin, respformation, or responsable_id # Check access: admin, respformation, or responsable_id
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id): if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
return json_error(403, "modification notes non autorisee pour cet utilisateur") result["status"] = "unauthorized"
# else:
valid_notes, _, _, _, _ = _check_notes(notes, evaluation) L, _, _, _, _ = _check_notes([(etudid, value)], E, Mod)
if valid_notes: if L:
etudids_changed, _, etudids_with_decision = notes_add( nbchanged, _, existing_decisions = notes_add(
current_user, evaluation.id, valid_notes, comment=comment, do_it=True current_user, evaluation_id, L, comment=comment, do_it=True
) )
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
obj=evaluation.moduleimpl_id, obj=M["moduleimpl_id"],
text=f"""Chargement notes dans <a href="{status_url}">{ text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
evaluation.moduleimpl.module.titre or evaluation.moduleimpl.module.code}</a>""", url=Mod["url"],
url=status_url,
max_frequency=30 * 60, # 30 minutes max_frequency=30 * 60, # 30 minutes
) )
result = { result["nbchanged"] = nbchanged
"etudids_with_decision": etudids_with_decision, result["existing_decisions"] = existing_decisions
"etudids_changed": etudids_changed, if nbchanged > 0:
"history_menu": { result["history_menu"] = get_note_history_menu(evaluation_id, etudid)
etudid: get_note_history_menu(evaluation.id, etudid)
for etudid in etudids_changed
},
}
else: else:
result = { result["history_menu"] = "" # no update needed
"etudids_changed": [], result["status"] = "ok"
"etudids_with_decision": [], return scu.sendJSON(result)
"history_menu": [],
}
return result
def get_note_history_menu(evaluation_id: int, etudid: int) -> str: def get_note_history_menu(evaluation_id, etudid):
"""Menu HTML historique de la note""" """Menu HTML historique de la note"""
history = sco_undo_notes.get_note_history(evaluation_id, etudid) history = sco_undo_notes.get_note_history(evaluation_id, etudid)
if not history: if not history:

View File

@ -1,32 +1,35 @@
// Formulaire saisie des notes // Formulaire saisie des notes
$().ready(function () { $().ready(function () {
$("#formnotes .note").bind("blur", valid_note); $("#formnotes .note").bind("blur", valid_note);
$("#formnotes input").bind("paste", paste_text); $("#formnotes input").bind("paste", paste_text);
$(".btn_masquer_DEM").bind("click", masquer_DEM); $(".btn_masquer_DEM").bind("click", masquer_DEM);
}); });
function is_valid_note(v) { function is_valid_note(v) {
if (!v) return true; if (!v)
return true;
var note_min = parseFloat($("#eval_note_min").text()); var note_min = parseFloat($("#eval_note_min").text());
var note_max = parseFloat($("#eval_note_max").text()); var note_max = parseFloat($("#eval_note_max").text());
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); var x = parseFloat(v);
return x >= note_min && x <= note_max; return (x >= note_min) && (x <= note_max);
} }
} }
function valid_note(e) { function valid_note(e) {
var v = this.value.trim().toUpperCase().replace(",", "."); var v = this.value.trim().toUpperCase().replace(",", ".");
if (is_valid_note(v)) { if (is_valid_note(v)) {
if (v && v != $(this).attr("data-last-saved-value")) { if (v && (v != $(this).attr('data-last-saved-value'))) {
this.className = "note_valid_new"; this.className = "note_valid_new";
const etudid = parseInt($(this).attr("data-etudid")); var etudid = $(this).attr('data-etudid');
save_note(this, v, etudid); save_note(this, v, etudid);
} }
} else { } else {
@ -36,82 +39,66 @@ function valid_note(e) {
} }
} }
async function save_note(elem, v, etudid) { function save_note(elem, v, etudid) {
let evaluation_id = $("#formnotes_evaluation_id").attr("value"); var evaluation_id = $("#formnotes_evaluation_id").attr("value");
let formsemestre_id = $("#formnotes_formsemestre_id").attr("value"); var formsemestre_id = $("#formnotes_formsemestre_id").attr("value");
$("#sco_msg").html("en cours...").show(); $('#sco_msg').html("en cours...").show();
try { $.post(SCO_URL + '/Notes/save_note',
const response = await fetch(
SCO_URL + "/../api/evaluation/" + evaluation_id + "/notes/set",
{ {
method: "POST", 'etudid': etudid,
headers: { 'evaluation_id': evaluation_id,
"Content-Type": "application/json", 'value': v,
'comment': document.getElementById('formnotes_comment').value
}, },
body: JSON.stringify({ function (result) {
notes: [[etudid, v]], $('#sco_msg').hide();
comment: document.getElementById("formnotes_comment").value, if (result['nbchanged'] > 0) {
}),
}
);
if (!response.ok) {
sco_message("Erreur: valeur non enregistrée");
} else {
const data = await response.json();
$("#sco_msg").hide();
if (data.etudids_changed.length > 0) {
sco_message("enregistré"); sco_message("enregistré");
elem.className = "note_saved"; elem.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 (result.existing_decisions[0] == etudid) {
if (v != $(elem).attr("data-orig-value")) { if (v != $(elem).attr('data-orig-value')) {
$("#jurylink_" + etudid).html( $("#jurylink_" + etudid).html('<a href="formsemestre_validation_etud_form?formsemestre_id=' + formsemestre_id + '&etudid=' + etudid + '">mettre à jour décision de jury</a>');
'<a href="formsemestre_validation_etud_form?formsemestre_id=' +
formsemestre_id +
"&etudid=" +
etudid +
'">mettre à jour décision de jury</a>'
);
} else { } else {
$("#jurylink_" + etudid).html(""); $("#jurylink_" + etudid).html('');
} }
} }
// Mise à jour menu historique // mise a jour menu historique
if (data.history_menu[etudid]) { if (result['history_menu']) {
$("#hist_" + etudid).html(data.history_menu[etudid]); $("#hist_" + etudid).html(result['history_menu']);
} }
$(elem).attr("data-last-saved-value", v); $(elem).attr('data-last-saved-value', v);
} else {
$('#sco_msg').html("").show();
sco_message("valeur non enregistrée");
} }
} }
} catch (error) { );
console.error("Fetch error:", error);
sco_message("Erreur réseau: valeur non enregistrée");
}
} }
function change_history(e) { function change_history(e) {
let opt = e.selectedOptions[0]; var opt = e.selectedOptions[0];
let val = $(opt).attr("data-note"); var val = $(opt).attr("data-note");
const etudid = parseInt($(e).attr("data-etudid")); var etudid = $(e).attr('data-etudid');
// le input associé a ce menu: // le input associé a ce menu:
let input_elem = e.parentElement.parentElement.parentElement.childNodes[0]; var input_elem = e.parentElement.parentElement.parentElement.childNodes[0];
input_elem.value = val; input_elem.value = val;
save_note(input_elem, val, etudid); save_note(input_elem, val, etudid);
} }
// Contribution S.L.: copier/coller des notes // Contribution S.L.: copier/coller des notes
function paste_text(e) { function paste_text(e) {
var event = e.originalEvent; var event = e.originalEvent;
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
var clipb = e.originalEvent.clipboardData; var clipb = e.originalEvent.clipboardData;
var data = clipb.getData("Text"); var data = clipb.getData('Text');
var list = data.split(/\r\n|\r|\n|\t| /g); var list = data.split(/\r\n|\r|\n|\t| /g);
var currentInput = event.currentTarget; var currentInput = event.currentTarget;
var masquerDEM = document var masquerDEM = document.querySelector("body").classList.contains("masquer_DEM");
.querySelector("body")
.classList.contains("masquer_DEM");
for (var i = 0; i < list.length; i++) { for (var i = 0; i < list.length; i++) {
currentInput.value = list[i]; currentInput.value = list[i];
@ -121,8 +108,12 @@ function paste_text(e) {
var sibbling = currentInput.parentElement.parentElement.nextElementSibling; var sibbling = currentInput.parentElement.parentElement.nextElementSibling;
while ( while (
sibbling && sibbling &&
(sibbling.style.display == "none" || (
(masquerDEM && sibbling.classList.contains("etud_dem"))) sibbling.style.display == "none" ||
(
masquerDEM && sibbling.classList.contains("etud_dem")
)
)
) { ) {
sibbling = sibbling.nextElementSibling; sibbling = sibbling.nextElementSibling;
} }

View File

@ -1875,6 +1875,12 @@ sco_publish(
Permission.ScoEnsView, Permission.ScoEnsView,
) )
sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.ScoEnsView) sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.ScoEnsView)
sco_publish(
"/save_note",
sco_saisie_notes.save_note,
Permission.ScoEnsView,
methods=["GET", "POST"],
)
sco_publish( sco_publish(
"/do_evaluation_set_missing", "/do_evaluation_set_missing",
sco_saisie_notes.do_evaluation_set_missing, sco_saisie_notes.do_evaluation_set_missing,

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.81" SCOVERSION = "9.4.80"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -24,7 +24,7 @@ from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
verify_fields, verify_fields,
EVALUATIONS_FIELDS, EVALUATIONS_FIELDS,
NOTES_FIELDS, EVALUATION_FIELDS,
) )
@ -35,7 +35,7 @@ def test_evaluations(api_headers):
Route : Route :
- /moduleimpl/<int:moduleimpl_id>/evaluations - /moduleimpl/<int:moduleimpl_id>/evaluations
""" """
moduleimpl_id = 20 moduleimpl_id = 1
r = requests.get( r = requests.get(
f"{API_URL}/moduleimpl/{moduleimpl_id}/evaluations", f"{API_URL}/moduleimpl/{moduleimpl_id}/evaluations",
headers=api_headers, headers=api_headers,
@ -44,7 +44,6 @@ def test_evaluations(api_headers):
) )
assert r.status_code == 200 assert r.status_code == 200
list_eval = r.json() list_eval = r.json()
assert list_eval
assert isinstance(list_eval, list) assert isinstance(list_eval, list)
for eval in list_eval: for eval in list_eval:
assert verify_fields(eval, EVALUATIONS_FIELDS) is True assert verify_fields(eval, EVALUATIONS_FIELDS) is True
@ -64,14 +63,16 @@ def test_evaluations(api_headers):
assert eval["moduleimpl_id"] == moduleimpl_id assert eval["moduleimpl_id"] == moduleimpl_id
def test_evaluation_notes(api_headers): def test_evaluation_notes(
api_headers,
): # XXX TODO changer la boucle pour parcourir le dict sans les indices
""" """
Test 'evaluation_notes' Test 'evaluation_notes'
Route : Route :
- /evaluation/<int:evaluation_id>/notes - /evaluation/<int:evaluation_id>/notes
""" """
eval_id = 20 eval_id = 1
r = requests.get( r = requests.get(
f"{API_URL}/evaluation/{eval_id}/notes", f"{API_URL}/evaluation/{eval_id}/notes",
headers=api_headers, headers=api_headers,
@ -80,15 +81,14 @@ def test_evaluation_notes(api_headers):
) )
assert r.status_code == 200 assert r.status_code == 200
eval_notes = r.json() eval_notes = r.json()
assert eval_notes for i in range(1, len(eval_notes)):
for etudid, note in eval_notes.items(): assert verify_fields(eval_notes[f"{i}"], EVALUATION_FIELDS)
assert int(etudid) == note["etudid"] assert isinstance(eval_notes[f"{i}"]["id"], int)
assert verify_fields(note, NOTES_FIELDS) assert isinstance(eval_notes[f"{i}"]["etudid"], int)
assert isinstance(note["etudid"], int) assert isinstance(eval_notes[f"{i}"]["evaluation_id"], int)
assert isinstance(note["evaluation_id"], int) assert isinstance(eval_notes[f"{i}"]["value"], float)
assert isinstance(note["value"], float) assert isinstance(eval_notes[f"{i}"]["comment"], str)
assert isinstance(note["comment"], str) assert isinstance(eval_notes[f"{i}"]["date"], str)
assert isinstance(note["date"], str) assert isinstance(eval_notes[f"{i}"]["uid"], int)
assert isinstance(note["uid"], int)
assert eval_id == note["evaluation_id"] assert eval_id == eval_notes[f"{i}"]["evaluation_id"]

View File

@ -58,7 +58,6 @@ def test_permissions(api_headers):
"nip": 1, "nip": 1,
"partition_id": 1, "partition_id": 1,
"role_name": "Ens", "role_name": "Ens",
"start": "abc",
"uid": 1, "uid": 1,
"version": "long", "version": "long",
"assiduite_id": 1, "assiduite_id": 1,

View File

@ -568,7 +568,8 @@ EVALUATIONS_FIELDS = {
"visi_bulletin", "visi_bulletin",
} }
NOTES_FIELDS = { EVALUATION_FIELDS = {
"id",
"etudid", "etudid",
"evaluation_id", "evaluation_id",
"value", "value",

View File

@ -103,14 +103,14 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# --- Saisie toutes les notes de l'évaluation # --- Saisie toutes les notes de l'évaluation
for idx, etud in enumerate(etuds): for idx, etud in enumerate(etuds):
etudids_changed, nb_suppress, existing_decisions = G.create_note( nb_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e["id"], evaluation_id=e["id"],
etudid=etud["etudid"], etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)], note=NOTES_T[idx % len(NOTES_T)],
) )
assert not existing_decisions assert not existing_decisions
assert nb_suppress == 0 assert nb_suppress == 0
assert len(etudids_changed) == 1 assert nb_changed == 1
# --- Vérifie que les notes sont prises en compte: # --- Vérifie que les notes sont prises en compte:
b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"]) b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"])
@ -136,7 +136,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
) )
# Saisie les notes des 5 premiers étudiants: # Saisie les notes des 5 premiers étudiants:
for idx, etud in enumerate(etuds[:5]): for idx, etud in enumerate(etuds[:5]):
etudids_changed, nb_suppress, existing_decisions = G.create_note( nb_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["id"], evaluation_id=e2["id"],
etudid=etud["etudid"], etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)], note=NOTES_T[idx % len(NOTES_T)],
@ -158,7 +158,7 @@ def run_sco_basic(verbose=False) -> FormSemestre:
# Saisie des notes qui manquent: # Saisie des notes qui manquent:
for idx, etud in enumerate(etuds[5:]): for idx, etud in enumerate(etuds[5:]):
etudids_changed, nb_suppress, existing_decisions = G.create_note( nb_changed, nb_suppress, existing_decisions = G.create_note(
evaluation_id=e2["id"], evaluation_id=e2["id"],
etudid=etud["etudid"], etudid=etud["etudid"],
note=NOTES_T[idx % len(NOTES_T)], note=NOTES_T[idx % len(NOTES_T)],

View File

@ -242,7 +242,7 @@ def create_evaluations(formsemestre: FormSemestre):
"jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=modimpl.id), "jour": datetime.date(2022, 3, 1) + datetime.timedelta(days=modimpl.id),
"heure_debut": "8h00", "heure_debut": "8h00",
"heure_fin": "9h00", "heure_fin": "9h00",
"description": f"Evaluation-{modimpl.module.code}", "description": None,
"note_max": 20, "note_max": 20,
"coefficient": 1.0, "coefficient": 1.0,
"visibulletin": True, "visibulletin": True,