diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 6643ea8444..b1ffee7883 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -273,5 +273,5 @@ def evaluation_delete(evaluation_id: int): sco_saisie_notes.evaluation_suppress_alln( evaluation_id=evaluation_id, dialog_confirmed=True ) - sco_evaluation_db.do_evaluation_delete(evaluation_id) + evaluation.delete() return "ok" diff --git a/app/models/evaluations.py b/app/models/evaluations.py index 235b7f74d9..73659aa3d3 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -141,6 +141,44 @@ class Evaluation(db.Model): n = 0 # the only one return n + def delete(self): + "delete evaluation (commit) (check permission)" + from app.scodoc import sco_evaluation_db + + modimpl: ModuleImpl = self.moduleimpl + if not modimpl.can_edit_evaluation(current_user): + raise AccessDenied( + f"Modification évaluation impossible pour {current_user.get_nomplogin()}" + ) + notes_db = sco_evaluation_db.do_evaluation_get_all_notes( + self.id + ) # { etudid : value } + notes = [x["value"] for x in notes_db.values()] + if notes: + raise ScoValueError( + "Impossible de supprimer cette évaluation: il reste des notes" + ) + log(f"deleting evaluation {self}") + db.session.delete(self) + db.session.commit() + + # inval cache pour ce semestre + sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) + # news + url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=modimpl.id, + ) + ScolarNews.add( + typ=ScolarNews.NEWS_NOTE, + obj=modimpl.id, + text=f"""Suppression d'une évaluation dans {modimpl.module.titre}""", + url=url, + ) + def to_dict(self) -> dict: "Représentation dict (riche, compat ScoDoc 7)" e_dict = dict(self.__dict__) diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index e84fe82b39..a44a4b5fcf 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -118,6 +118,32 @@ class ModuleImpl(db.Model): return False + def can_edit_notes(self, user: "User", allow_ens=True) -> bool: + """True if authuser can enter or edit notes in this module. + If allow_ens, grant access to all ens in this module + + Si des décisions de jury ont déjà été saisies dans ce semestre, + seul le directeur des études peut saisir des notes (et il ne devrait pas). + """ + # was sco_permissions_check.can_edit_notes + from app.scodoc import sco_cursus_dut + + if not self.formsemestre.etat: + return False # semestre verrouillé + is_dir_etud = user.id in (u.id for u in self.formsemestre.responsables) + can_edit_all_notes = user.has_permission(Permission.ScoEditAllNotes) + if sco_cursus_dut.formsemestre_has_decisions(self.formsemestre_id): + # il y a des décisions de jury dans ce semestre ! + return can_edit_all_notes or is_dir_etud + if ( + not can_edit_all_notes + and user.id != self.responsable_id + and not is_dir_etud + ): + # enseignant (chargé de TD) ? + return allow_ens and user.id in (ens.id for ens in self.enseignants) + return True + def can_change_ens_by(self, user: User, raise_exc=False) -> bool: """Check if user can modify module resp. If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not. diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 041daf3423..38110c6c8d 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -28,19 +28,17 @@ """Gestion évaluations (ScoDoc7, code en voie de modernisation) """ -import pprint - import flask from flask import url_for, g from flask_login import current_user from app import db, log -from app.models import Evaluation, ModuleImpl, ScolarNews +from app.models import Evaluation from app.models.evaluations import check_convert_evaluation_args import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app.scodoc.sco_exceptions import AccessDenied, ScoValueError +from app.scodoc.sco_exceptions import AccessDenied from app.scodoc import sco_cache from app.scodoc import sco_moduleimpl @@ -119,42 +117,6 @@ def do_evaluation_edit(args): ) -def do_evaluation_delete(evaluation_id): - "delete evaluation" - evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) - modimpl: ModuleImpl = evaluation.moduleimpl - if not modimpl.can_edit_evaluation(current_user): - raise AccessDenied( - f"Modification évaluation impossible pour {current_user.get_nomplogin()}" - ) - notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value } - notes = [x["value"] for x in notes_db.values()] - if notes: - raise ScoValueError( - "Impossible de supprimer cette évaluation: il reste des notes" - ) - log(f"deleting evaluation {evaluation}") - db.session.delete(evaluation) - db.session.commit() - - # inval cache pour ce semestre - sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) - # news - url = url_for( - "notes.moduleimpl_status", - scodoc_dept=g.scodoc_dept, - moduleimpl_id=modimpl.id, - ) - ScolarNews.add( - typ=ScolarNews.NEWS_NOTE, - obj=modimpl.id, - text=f"""Suppression d'une évaluation dans {modimpl.module.titre}""", - url=url, - ) - - # ancien _notes_getall def do_evaluation_get_all_notes( evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 43959272b9..433f0d445c 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -30,16 +30,17 @@ import collections import datetime import operator -import time from flask import url_for from flask import g from flask_login import current_user from flask import request +from app import db +from app.auth.models import User from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import Evaluation, FormSemestre import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType @@ -645,78 +646,64 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True): """HTML description of evaluation, for page headers edit_in_place: allow in-place editing when permitted (not implemented) """ - E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0] - moduleimpl_id = E["moduleimpl_id"] - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - formsemestre_id = M["formsemestre_id"] - u = sco_users.user_info(M["responsable_id"]) - resp = u["prenomnom"] - nomcomplet = u["nomcomplet"] - can_edit = sco_permissions_check.can_edit_notes( - current_user, moduleimpl_id, allow_ens=False - ) + evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id) + modimpl = evaluation.moduleimpl + responsable: User = db.session.get(User, modimpl.responsable_id) + resp_nomprenom = responsable.get_prenomnom() + resp_nomcomplet = responsable.get_nomcomplet() + can_edit = modimpl.can_edit_notes(current_user, allow_ens=False) - link = ( - 'voir toutes les notes du module' - % moduleimpl_id - ) - mod_descr = ( - '%s %s (resp. %s) %s' - % ( - moduleimpl_id, - Mod["code"] or "", - Mod["titre"] or "?", - nomcomplet, - resp, - link, - ) - ) + mod_descr = f"""{modimpl.module.code or ""} {modimpl.module.abbrev or modimpl.module.titre or "?"} + (resp. {resp_nomprenom}) + voir toutes les notes du module + """ - etit = E["description"] or "" - if etit: - etit = ' "' + etit + '"' - if Mod["module_type"] == ModuleType.MALUS: - etit += ' (points de malus)' + eval_titre = f' "{evaluation.description}"' if evaluation.description else "" + if modimpl.module.module_type == ModuleType.MALUS: + eval_titre += ' (points de malus)' H = [ - 'Évaluation%s
Module : %s
' - % (etit, mod_descr) + f"""Évaluation{eval_titre} +Module : {mod_descr} +
""" ] - if Mod["module_type"] == ModuleType.MALUS: + if modimpl.module.module_type == ModuleType.MALUS: # Indique l'UE - ue = sco_edit_ue.ue_list(args={"ue_id": Mod["ue_id"]})[0] - H.append("UE : %(acronyme)s
" % ue) + ue = modimpl.module.ue + H.append(f"UE : {ue.acronyme}
") # store min/max values used by JS client-side checks: H.append( - '-20.20.' + """-20. + 20.""" ) else: # date et absences (pas pour evals de malus) - if E["jour"]: - jour = E["jour"] - H.append("Réalisée le %s " % (jour)) - if E["heure_debut"] != E["heure_fin"]: - H.append("de %s à %s " % (E["heure_debut"], E["heure_fin"])) - group_id = sco_groups.get_default_group(formsemestre_id) + if evaluation.date_debut is not None: + H.append(f"
Réalisée le {evaluation.descr_date()} ") + group_id = sco_groups.get_default_group(modimpl.formsemestre_id) H.append( - f"""
""" + }">absences ce jour""" ) else: - jour = "pas de date" - H.append("Réalisée le %s " % (jour)) + H.append("
sans date ") H.append( - '
Coefficient dans le module: %s, notes sur %g ' - % (E["coefficient"], E["note_max"]) + f"""
Coefficient dans le module: {evaluation.coefficient or "0"}, + notes sur {(evaluation.note_max or 0):g} """ ) H.append('0.') if can_edit: @@ -730,7 +717,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True): if link_saisie: H.append( f""" - saisie des notes """ diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 3cfb4cd867..1bcfd9702c 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -50,7 +50,6 @@ from app.scodoc import htmlutils from app.scodoc import sco_cal from app.scodoc import sco_compute_moy from app.scodoc import sco_evaluations -from app.scodoc import sco_evaluation_db from app.scodoc import sco_formsemestre_status from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl @@ -59,19 +58,15 @@ from app.tables import list_etuds # menu evaluation dans moduleimpl -def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: +def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str: "Menu avec actions sur une evaluation" - E = sco_evaluation_db.get_evaluation_dict({"evaluation_id": evaluation_id})[0] - modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] + modimpl: ModuleImpl = evaluation.moduleimpl + group_id = sco_groups.get_default_group(modimpl.formsemestre_id) + evaluation_id = evaluation.id + can_edit_notes = modimpl.can_edit_notes(current_user, allow_ens=False) + can_edit_notes_ens = modimpl.can_edit_notes(current_user) - group_id = sco_groups.get_default_group(modimpl["formsemestre_id"]) - - if ( - sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"], allow_ens=False - ) - and nbnotes != 0 - ): + if can_edit_notes and nbnotes != 0: sup_label = "Supprimer évaluation impossible (il y a des notes)" else: sup_label = "Supprimer évaluation" @@ -83,9 +78,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: "args": { "evaluation_id": evaluation_id, }, - "enabled": sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"] - ), + "enabled": can_edit_notes_ens, }, { "title": "Modifier évaluation", @@ -93,9 +86,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: "args": { "evaluation_id": evaluation_id, }, - "enabled": sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"], allow_ens=False - ), + "enabled": can_edit_notes, }, { "title": sup_label, @@ -103,10 +94,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: "args": { "evaluation_id": evaluation_id, }, - "enabled": nbnotes == 0 - and sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"], allow_ens=False - ), + "enabled": nbnotes == 0 and can_edit_notes, }, { "title": "Supprimer toutes les notes", @@ -114,9 +102,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: "args": { "evaluation_id": evaluation_id, }, - "enabled": sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"], allow_ens=False - ), + "enabled": can_edit_notes, }, { "title": "Afficher les notes", @@ -132,21 +118,18 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: "args": { "evaluation_id": evaluation_id, }, - "enabled": sco_permissions_check.can_edit_notes( - current_user, E["moduleimpl_id"] - ), + "enabled": can_edit_notes_ens, }, { "title": "Absences ce jour", - "endpoint": "assiduites.get_etat_abs_date", + "endpoint": "assiduites.etat_abs_date", "args": { "group_ids": group_id, - "desc": E["description"], - "jour": E["jour"], - "heure_debut": E["heure_debut"], - "heure_fin": E["heure_fin"], + "desc": evaluation.description or "", + "date_debut": evaluation.date_debut.isoformat(), + "date_fin": evaluation.date_fin.isoformat(), }, - "enabled": E["jour"], + "enabled": evaluation.date_debut is not None, }, { "title": "Vérifier notes vs absents", @@ -154,7 +137,7 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str: "args": { "evaluation_id": evaluation_id, }, - "enabled": nbnotes > 0 and E["jour"], + "enabled": nbnotes > 0 and evaluation.date_debut is not None, }, ] @@ -714,7 +697,7 @@ def _ligne_evaluation( if can_edit_notes: H.append( moduleimpl_evaluation_menu( - evaluation.id, + evaluation, nbnotes=etat["nb_notes"], ) ) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 92d00c3769..9312a5d650 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1315,7 +1315,7 @@ a.smallbutton { } span.evallink { - font-size: 80%; + margin-left: 16px; font-weight: normal; } diff --git a/app/views/assiduites.py b/app/views/assiduites.py index d4608dc55a..ec81393988 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -1,8 +1,8 @@ import datetime from flask import g, request, render_template - from flask import abort, url_for +from flask_login import current_user from app import db from app.comp import res_sem @@ -25,14 +25,16 @@ from app.views import ScoData # --------------- from app.scodoc.sco_permissions import Permission from app.scodoc import html_sco_header +from app.scodoc import safehtml from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences from app.scodoc import sco_groups_view from app.scodoc import sco_etud from app.scodoc import sco_find_etud -from flask_login import current_user -from app.scodoc import sco_utils as scu from app.scodoc import sco_assiduites as scass +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError + from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids @@ -731,17 +733,23 @@ def visu_assiduites_group(): ).build() -@bp.route("/EtatAbsencesDate") +@bp.route("/etat_abs_date") @scodoc @permission_required(Permission.ScoView) -def get_etat_abs_date(): - infos_date = { - "jour": request.args.get("jour"), - "heure_debut": request.args.get("heure_debut"), - "heure_fin": request.args.get("heure_fin"), - "title": request.args.get("desc"), - } +def etat_abs_date(): + """date_debut, date_fin en ISO""" + date_debut_str = request.args.get("date_debut") + date_fin_str = request.args.get("date_fin") + title = request.args.get("desc") group_ids: list[int] = request.args.get("group_ids", None) + try: + date_debut = datetime.datetime.fromisoformat(date_debut_str) + except ValueError as exc: + raise ScoValueError("date_debut invalide") from exc + try: + date_fin = datetime.datetime.fromisoformat(date_fin_str) + except ValueError as exc: + raise ScoValueError("date_fin invalide") from exc if group_ids is None: group_ids = [] else: @@ -754,14 +762,6 @@ def get_etat_abs_date(): sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] - - date_debut = scu.is_iso_formated( - f"{infos_date['jour']}T{infos_date['heure_debut'].replace('h',':')}", True - ) - date_fin = scu.is_iso_formated( - f"{infos_date['jour']}T{infos_date['heure_fin'].replace('h',':')}", True - ) - assiduites: Assiduite = Assiduite.query.filter( Assiduite.etudid.in_([e["etudid"] for e in etuds]) ) @@ -791,7 +791,7 @@ def get_etat_abs_date(): etudiants = list(sorted(etudiants, key=lambda x: x["nom"])) header: str = html_sco_header.sco_header( - page_title=infos_date["title"], + page_title=safehtml.html_to_safe_html(title), init_qtip=True, ) diff --git a/app/views/notes.py b/app/views/notes.py index 11c25dbe58..9f69cbd97f 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1656,30 +1656,37 @@ sco_publish( @scodoc7func def evaluation_delete(evaluation_id): """Form delete evaluation""" - El = sco_evaluation_db.get_evaluation_dict(args={"evaluation_id": evaluation_id}) - if not El: - raise ScoValueError("Evaluation inexistante ! (%s)" % evaluation_id) - E = El[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] - tit = "Suppression de l'évaluation %(description)s (%(jour)s)" % E - etat = sco_evaluations.do_evaluation_etat(evaluation_id) + evaluation: Evaluation = ( + Evaluation.query.filter_by(id=evaluation_id) + .join(ModuleImpl) + .join(FormSemestre) + .filter_by(dept_id=g.scodoc_dept_id) + .first_or_404() + ) + + tit = f"""Suppression de l'évaluation {evaluation.description or ""} ({evaluation.descr_date()})""" + etat = sco_evaluations.do_evaluation_etat(evaluation.id) H = [ - html_sco_header.html_sem_header(tit, with_h2=False), - """
Opération irréversible. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.
""", + f""" + {html_sco_header.html_sem_header(tit, with_h2=False)} +Opération irréversible. + Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées. +
+ """, ] warning = False if etat["nb_notes_total"]: warning = True nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"] H.append( - """Suppression impossible (effacer les notes d'abord)
Suppression impossible (effacer les notes d'abord)
+ +OK, évaluation supprimée.
""" + html_sco_header.sco_footer() ) diff --git a/sco_version.py b/sco_version.py index b5137adad4..c72a265843 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.15" +SCOVERSION = "9.6.16" SCONAME = "ScoDoc" diff --git a/tests/unit/test_notes_rattrapage.py b/tests/unit/test_notes_rattrapage.py index 45433886fd..918a357c5b 100644 --- a/tests/unit/test_notes_rattrapage.py +++ b/tests/unit/test_notes_rattrapage.py @@ -6,10 +6,9 @@ import app from app import db from app.comp import res_sem from app.comp.res_but import ResultatsSemestreBUT -from app.models import FormSemestre, ModuleImpl +from app.models import Evaluation, FormSemestre, ModuleImpl from app.scodoc import ( sco_bulletins, - sco_evaluation_db, sco_formsemestre, sco_saisie_notes, ) @@ -131,7 +130,9 @@ def test_notes_rattrapage(test_client): # Note moyenne: reviens à 10/20 assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0) # Supprime l'évaluation de rattrapage: - sco_evaluation_db.do_evaluation_delete(e_rat["id"]) + evaluation = db.session.get(Evaluation, e_rat["id"]) + assert evaluation + evaluation.delete() b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] )