diff --git a/app/__init__.py b/app/__init__.py index 0943a91f6a..04e28207c1 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -190,6 +190,7 @@ def create_app(config_class=DevConfig): app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error) + app.register_error_handler(AccessDenied, handle_access_denied) app.register_error_handler(500, internal_server_error) app.register_error_handler(503, postgresql_server_error) diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 773cf81d1e..05b72b011f 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -78,7 +78,8 @@ def html_edit_formation_apc( alt="supprimer", ), "delete_disabled": scu.icontag( - "delete_small_dis_img", title="Suppression impossible (module utilisé)" + "delete_small_dis_img", + title="Suppression impossible (utilisé dans des semestres)", ), } diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index 62a7e1d201..13ddb2a99e 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -30,13 +30,18 @@ """ import flask from flask import g, url_for, request +from app.models.formations import Matiere import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log from app.models import Formation from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message -from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError +from app.scodoc.sco_exceptions import ( + ScoValueError, + ScoLockedFormError, + ScoNonEmptyFormationObject, +) from app.scodoc import html_sco_header _matiereEditor = ndb.EditableTable( @@ -156,6 +161,16 @@ associé. return flask.redirect(dest_url) +def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]: + "True si la matiere n'est pas utilisée dans des formsemestre" + locked = matiere_is_locked(matiere.id) + if locked: + return False + if any(m.modimpls.all() for m in matiere.modules): + return False + return True + + def do_matiere_delete(oid): "delete matiere and attached modules" from app.scodoc import sco_formations @@ -165,17 +180,16 @@ def do_matiere_delete(oid): cnx = ndb.GetDBConnexion() # check - mat = matiere_list({"matiere_id": oid})[0] + matiere = Matiere.query.get_or_404(oid) + mat = matiere_list({"matiere_id": oid})[0] # compat sco7 ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0] - locked = matiere_is_locked(mat["matiere_id"]) - if locked: - log("do_matiere_delete: mat=%s" % mat) - log("do_matiere_delete: ue=%s" % ue) - log("do_matiere_delete: locked sems: %s" % locked) - raise ScoLockedFormError() - log("do_matiere_delete: matiere_id=%s" % oid) + if not can_delete_matiere(matiere): + # il y a au moins un modimpl dans un module de cette matière + raise ScoNonEmptyFormationObject("Matière", matiere.titre) + + log("do_matiere_delete: matiere_id=%s" % matiere.id) # delete all modules in this matiere - mods = sco_edit_module.module_list({"matiere_id": oid}) + mods = sco_edit_module.module_list({"matiere_id": matiere.id}) for mod in mods: sco_edit_module.do_module_delete(mod["module_id"]) _matiereEditor.delete(cnx, oid) @@ -194,11 +208,25 @@ def matiere_delete(matiere_id=None): """Delete matière""" from app.scodoc import sco_edit_ue - M = matiere_list(args={"matiere_id": matiere_id})[0] - UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0] + matiere = Matiere.query.get_or_404(matiere_id) + if not can_delete_matiere(matiere): + # il y a au moins un modimpl dans un module de cette matière + raise ScoNonEmptyFormationObject( + "Matière", + matiere.titre, + dest_url=url_for( + "notes.ue_table", + formation_id=matiere.ue.formation_id, + semestre_idx=matiere.ue.semestre_idx, + scodoc_dept=g.scodoc_dept, + ), + ) + + mat = matiere_list(args={"matiere_id": matiere_id})[0] + UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0] H = [ html_sco_header.sco_header(page_title="Suppression d'une matière"), - "

Suppression de la matière %(titre)s" % M, + "

Suppression de la matière %(titre)s" % mat, " dans l'UE (%(acronyme)s))

" % UE, ] dest_url = url_for( @@ -210,7 +238,7 @@ def matiere_delete(matiere_id=None): request.base_url, scu.get_request_args(), (("matiere_id", {"input_type": "hidden"}),), - initvalues=M, + initvalues=mat, submitlabel="Confirmer la suppression", cancelbutton="Annuler", ) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 1c3481b066..813320d534 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -43,7 +43,12 @@ from app import models from app.models import Formation from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError +from app.scodoc.sco_exceptions import ( + ScoValueError, + ScoLockedFormError, + ScoGenError, + ScoNonEmptyFormationObject, +) from app.scodoc import html_sco_header from app.scodoc import sco_codes_parcours from app.scodoc import sco_edit_matiere @@ -330,20 +335,37 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): ) +def can_delete_module(module): + "True si le module n'est pas utilisée dans des formsemestre" + return len(module.modimpls.all()) == 0 + + def do_module_delete(oid): "delete module" from app.scodoc import sco_formations - mod = module_list({"module_id": oid})[0] - if module_is_locked(mod["module_id"]): + module = Module.query.get_or_404(oid) + mod = module_list({"module_id": oid})[0] # sco7 + if module_is_locked(module.id): raise ScoLockedFormError() + if not can_delete_module(module): + raise ScoNonEmptyFormationObject( + "Module", + msg=module.titre, + dest_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=module.formation_id, + semestre_idx=module.ue.semestre_idx, + ), + ) # S'il y a des moduleimpls, on ne peut pas detruire le module ! mods = sco_moduleimpl.moduleimpl_list(module_id=oid) if mods: err_page = f"""

Destruction du module impossible car il est utilisé dans des semestres existants !

-

Il faut d'abord supprimer le semestre. Mais il est peut être préférable de - laisser ce programme intact et d'en créer une nouvelle version pour la modifier. +

Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de + laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.

reprendre @@ -365,12 +387,21 @@ def do_module_delete(oid): def module_delete(module_id=None): """Delete a module""" - if not module_id: - raise ScoValueError("invalid module !") - modules = module_list(args={"module_id": module_id}) - if not modules: - raise ScoValueError("Module inexistant !") - mod = modules[0] + module = Module.query.get_or_404(module_id) + mod = module_list(args={"module_id": module_id})[0] # sco7 + + if not can_delete_module(module): + raise ScoNonEmptyFormationObject( + "Module", + msg=module.titre, + dest_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=module.formation_id, + semestre_idx=module.ue.semestre_idx, + ), + ) + H = [ html_sco_header.sco_header(page_title="Suppression d'un module"), """

Suppression du module %(titre)s (%(code)s)

""" % mod, diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index bdf37375bb..c93bdb1b99 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -42,7 +42,12 @@ from app import log from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.gen_tables import GenTable from app.scodoc.sco_permissions import Permission -from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError +from app.scodoc.sco_exceptions import ( + ScoGenError, + ScoValueError, + ScoLockedFormError, + ScoNonEmptyFormationObject, +) from app.scodoc import html_sco_header from app.scodoc import sco_cache @@ -130,61 +135,80 @@ def do_ue_create(args): return ue_id +def can_delete_ue(ue: UniteEns) -> bool: + """True si l'UE n'est pas utilisée dans des formsemestre + et n'a pas de module rattachés + """ + # "pas un seul module de cette UE n'a de modimpl..."" + return (not len(ue.modules.all())) and not any(m.modimpls.all() for m in ue.modules) + + def do_ue_delete(ue_id, delete_validations=False, force=False): "delete UE and attached matieres (but not modules)" from app.scodoc import sco_formations from app.scodoc import sco_parcours_dut + ue = UniteEns.query.get_or_404(ue_id) + if not can_delete_ue(ue): + raise ScoNonEmptyFormationObject( + "UE", + msg=ue.titre, + dest_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=ue.formation_id, + semestre_idx=ue.semestre_idx, + ), + ) + cnx = ndb.GetDBConnexion() - log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations)) + log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations)) # check - ue = ue_list({"ue_id": ue_id}) - if not ue: - raise ScoValueError("UE inexistante !") - ue = ue[0] - if ue_is_locked(ue["ue_id"]): - raise ScoLockedFormError() + # if ue_is_locked(ue.id): + # raise ScoLockedFormError() # Il y a-t-il des etudiants ayant validé cette UE ? # si oui, propose de supprimer les validations validations = sco_parcours_dut.scolar_formsemestre_validation_list( - cnx, args={"ue_id": ue_id} + cnx, args={"ue_id": ue.id} ) if validations and not delete_validations and not force: return scu.confirm_dialog( "

%d étudiants ont validé l'UE %s (%s)

Si vous supprimez cette UE, ces validations vont être supprimées !

" - % (len(validations), ue["acronyme"], ue["titre"]), + % (len(validations), ue.acronyme, ue.titre), dest_url="", target_variable="delete_validations", cancel_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=str(ue["formation_id"]), + formation_id=ue.formation_id, + semestre_idx=ue.semestre_idx, ), - parameters={"ue_id": ue_id, "dialog_confirmed": 1}, + parameters={"ue_id": ue.id, "dialog_confirmed": 1}, ) if delete_validations: - log("deleting all validations of UE %s" % ue_id) + log("deleting all validations of UE %s" % ue.id) ndb.SimpleQuery( "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", - {"ue_id": ue_id}, + {"ue_id": ue.id}, ) # delete all matiere in this UE - mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) + mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) for mat in mats: sco_edit_matiere.do_matiere_delete(mat["matiere_id"]) # delete uecoef and events ndb.SimpleQuery( "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", - {"ue_id": ue_id}, + {"ue_id": ue.id}, ) - ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id}) + ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}) cnx = ndb.GetDBConnexion() - _ueEditor.delete(cnx, ue_id) - # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?): + _ueEditor.delete(cnx, ue.id) + # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement + # utilisé: acceptable de tout invalider): sco_cache.invalidate_formsemestre() # news - F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0] + F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] sco_news.add( typ=sco_news.NEWS_FORM, object=ue["formation_id"], @@ -198,10 +222,10 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=ue["formation_id"], + semestre_idx=ue.semestre_idx, ) ) - else: - return None + return None def ue_create(formation_id=None): @@ -211,8 +235,6 @@ def ue_create(formation_id=None): def ue_edit(ue_id=None, create=False, formation_id=None): """Modification ou création d'une UE""" - from app.scodoc import sco_formations - create = int(create) if not create: U = ue_list(args={"ue_id": ue_id}) @@ -444,24 +466,38 @@ def next_ue_numero(formation_id, semestre_id=None): def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): """Delete an UE""" - ues = ue_list(args={"ue_id": ue_id}) - if not ues: - raise ScoValueError("UE inexistante !") - ue = ues[0] - - if not dialog_confirmed: - return scu.confirm_dialog( - "

Suppression de l'UE %(titre)s (%(acronyme)s))

" % ue, - dest_url="", - parameters={"ue_id": ue_id}, - cancel_url=url_for( + ue = UniteEns.query.get_or_404(ue_id) + if ue.modules.all(): + raise ScoValueError( + f"""Suppression de l'UE {ue.titre} impossible car + des modules (ou SAÉ ou ressources) lui sont rattachés.""" + ) + if not can_delete_ue(ue): + raise ScoNonEmptyFormationObject( + "UE", + msg=ue.titre, + dest_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=str(ue["formation_id"]), + formation_id=ue.formation_id, + semestre_idx=ue.semestre_idx, ), ) - return do_ue_delete(ue_id, delete_validations=delete_validations) + if not dialog_confirmed: + return scu.confirm_dialog( + f"

Suppression de l'UE {ue.titre} ({ue.acronyme})

", + dest_url="", + parameters={"ue_id": ue.id}, + cancel_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=ue.formation_id, + semestre_idx=ue.semestre_idx, + ), + ) + + return do_ue_delete(ue.id, delete_validations=delete_validations) def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list @@ -1010,12 +1046,14 @@ def _ue_table_modules( H.append(arrow_none) im += 1 if mod["nb_moduleimpls"] == 0 and editable: - H.append( - '%s' - % (mod["module_id"], delete_icon) - ) + icon = delete_icon else: - H.append(delete_disabled_icon) + icon = delete_disabled_icon + H.append( + '%s' + % (mod["module_id"], icon) + ) + H.append("") mod_editable = ( diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 635a724b55..1099986bf0 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -52,7 +52,7 @@ class InvalidNoteValue(ScoException): # Exception qui stoque dest_url, utilisee dans Zope standard_error_message class ScoValueError(ScoException): def __init__(self, msg, dest_url=None): - ScoException.__init__(self, msg) + super().__init__(msg) self.dest_url = dest_url @@ -72,20 +72,35 @@ class ScoConfigurationError(ScoValueError): pass -class ScoLockedFormError(ScoException): - def __init__(self, msg=""): +class ScoLockedFormError(ScoValueError): + "Modification d'une formation verrouillée" + + def __init__(self, msg="", dest_url=None): msg = ( "Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). " + str(msg) ) - ScoException.__init__(self, msg) + super().__init__(msg=msg, dest_url=dest_url) + + +class ScoNonEmptyFormationObject(ScoValueError): + """On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent""" + + def __init__(self, type_objet="objet'", msg="", dest_url=None): + msg = f"""

{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.

+

Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}). + Mais il est peut-être préférable de laisser ce programme intact et d'en créer une + nouvelle version pour la modifier sans affecter les semestres déjà en place. +

+ """ + super().__init__(msg=msg, dest_url=dest_url) class ScoGenError(ScoException): "exception avec affichage d'une page explicative ad-hoc" def __init__(self, msg=""): - ScoException.__init__(self, msg) + super().__init__(msg) class AccessDenied(ScoGenError): @@ -101,7 +116,7 @@ class APIInvalidParams(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): - Exception.__init__(self) + super().__init__() self.message = message if status_code is not None: self.status_code = status_code diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index 69d6983913..12e0397335 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -23,14 +23,11 @@ {{icons.arrow_none|safe}} {% endif %} - {% if editable and not ue.modules.count() %} + {{icons.delete|safe}} - {% else %} - {{icons.delete_disabled|safe}} - {% endif %} - + }}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %} + {{ue.acronyme}} {{ue.titre}} diff --git a/app/templates/sco_value_error.html b/app/templates/sco_value_error.html index 9b7f3b5475..3c46484257 100644 --- a/app/templates/sco_value_error.html +++ b/app/templates/sco_value_error.html @@ -7,10 +7,9 @@ {{ exc | safe }} -

{% if g.scodoc_dept %} - retour page d'accueil - departement {{ g.scodoc_dept }} + continuer {% else %} retour page d'accueil {% endif %}