meilleure gestion des suppressions d'objets dans l'édition des formations

This commit is contained in:
Emmanuel Viennet 2022-01-04 17:33:02 +01:00
parent f2e21e0cc2
commit a1bb957eaf
8 changed files with 193 additions and 83 deletions

View File

@ -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)

View File

@ -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)",
),
}

View File

@ -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"),
"<h2>Suppression de la matière %(titre)s" % M,
"<h2>Suppression de la matière %(titre)s" % mat,
" dans l'UE (%(acronyme)s))</h2>" % 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",
)

View File

@ -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"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
<p class="help">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.
<p class="help">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.
</p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a>
@ -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"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,

View File

@ -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(
"<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
% (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,9 +222,9 @@ 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
@ -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(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % 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"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
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:
icon = delete_icon
else:
icon = delete_disabled_icon
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
% (mod["module_id"], icon)
)
else:
H.append(delete_disabled_icon)
H.append("</span>")
mod_editable = (

View File

@ -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"""<h3>{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.</h3>
<p class="help">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.
</p>
"""
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

View File

@ -23,13 +23,10 @@
{{icons.arrow_none|safe}}
{% endif %}
</span>
{% if editable and not ue.modules.count() %}
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">{{icons.delete|safe}}</a>
{% else %}
{{icons.delete_disabled|safe}}
{% endif %}
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a>
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"

View File

@ -7,10 +7,9 @@
{{ exc | safe }}
<p class="footer">
<p>
{% if g.scodoc_dept %}
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">retour page d'accueil
departement {{ g.scodoc_dept }}</a>
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">continuer</a>
{% else %}
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
{% endif %}