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(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, 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(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error) app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error) app.register_error_handler(503, postgresql_server_error)

View File

@ -78,7 +78,8 @@ def html_edit_formation_apc(
alt="supprimer", alt="supprimer",
), ),
"delete_disabled": scu.icontag( "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 import flask
from flask import g, url_for, request from flask import g, url_for, request
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.models import Formation from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message 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 from app.scodoc import html_sco_header
_matiereEditor = ndb.EditableTable( _matiereEditor = ndb.EditableTable(
@ -156,6 +161,16 @@ associé.
return flask.redirect(dest_url) 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): def do_matiere_delete(oid):
"delete matiere and attached modules" "delete matiere and attached modules"
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -165,17 +180,16 @@ def do_matiere_delete(oid):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # 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] ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
locked = matiere_is_locked(mat["matiere_id"]) if not can_delete_matiere(matiere):
if locked: # il y a au moins un modimpl dans un module de cette matière
log("do_matiere_delete: mat=%s" % mat) raise ScoNonEmptyFormationObject("Matière", matiere.titre)
log("do_matiere_delete: ue=%s" % ue)
log("do_matiere_delete: locked sems: %s" % locked) log("do_matiere_delete: matiere_id=%s" % matiere.id)
raise ScoLockedFormError()
log("do_matiere_delete: matiere_id=%s" % oid)
# delete all modules in this matiere # 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: for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"]) sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid) _matiereEditor.delete(cnx, oid)
@ -194,11 +208,25 @@ def matiere_delete(matiere_id=None):
"""Delete matière""" """Delete matière"""
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
M = matiere_list(args={"matiere_id": matiere_id})[0] matiere = Matiere.query.get_or_404(matiere_id)
UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0] 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 = [ H = [
html_sco_header.sco_header(page_title="Suppression d'une matière"), 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, " dans l'UE (%(acronyme)s))</h2>" % UE,
] ]
dest_url = url_for( dest_url = url_for(
@ -210,7 +238,7 @@ def matiere_delete(matiere_id=None):
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),), (("matiere_id", {"input_type": "hidden"}),),
initvalues=M, initvalues=mat,
submitlabel="Confirmer la suppression", submitlabel="Confirmer la suppression",
cancelbutton="Annuler", cancelbutton="Annuler",
) )

View File

@ -43,7 +43,12 @@ from app import models
from app.models import Formation from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission 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 html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere 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): def do_module_delete(oid):
"delete module" "delete module"
from app.scodoc import sco_formations from app.scodoc import sco_formations
mod = module_list({"module_id": oid})[0] module = Module.query.get_or_404(oid)
if module_is_locked(mod["module_id"]): mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id):
raise ScoLockedFormError() 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 ! # S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.moduleimpl_list(module_id=oid) mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods: if mods:
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3> 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 <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. laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p> </p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a> formation_id=mod["formation_id"])}">reprendre</a>
@ -365,12 +387,21 @@ def do_module_delete(oid):
def module_delete(module_id=None): def module_delete(module_id=None):
"""Delete a module""" """Delete a module"""
if not module_id: module = Module.query.get_or_404(module_id)
raise ScoValueError("invalid module !") mod = module_list(args={"module_id": module_id})[0] # sco7
modules = module_list(args={"module_id": module_id})
if not modules: if not can_delete_module(module):
raise ScoValueError("Module inexistant !") raise ScoNonEmptyFormationObject(
mod = modules[0] "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 = [ H = [
html_sco_header.sco_header(page_title="Suppression d'un module"), html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod, """<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.TrivialFormulator import TrivialFormulator, TF
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission 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 html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -130,61 +135,80 @@ def do_ue_create(args):
return ue_id 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): def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)" "delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_parcours_dut 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() 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 # check
ue = ue_list({"ue_id": ue_id}) # if ue_is_locked(ue.id):
if not ue: # raise ScoLockedFormError()
raise ScoValueError("UE inexistante !")
ue = ue[0]
if ue_is_locked(ue["ue_id"]):
raise ScoLockedFormError()
# Il y a-t-il des etudiants ayant validé cette UE ? # Il y a-t-il des etudiants ayant validé cette UE ?
# si oui, propose de supprimer les validations # si oui, propose de supprimer les validations
validations = sco_parcours_dut.scolar_formsemestre_validation_list( 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: if validations and not delete_validations and not force:
return scu.confirm_dialog( 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>" "<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="", dest_url="",
target_variable="delete_validations", target_variable="delete_validations",
cancel_url=url_for( cancel_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, 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: if delete_validations:
log("deleting all validations of UE %s" % ue_id) log("deleting all validations of UE %s" % ue.id)
ndb.SimpleQuery( ndb.SimpleQuery(
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", "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 # 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: for mat in mats:
sco_edit_matiere.do_matiere_delete(mat["matiere_id"]) sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events # delete uecoef and events
ndb.SimpleQuery( ndb.SimpleQuery(
"DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", "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() cnx = ndb.GetDBConnexion()
_ueEditor.delete(cnx, ue_id) _ueEditor.delete(cnx, ue.id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?): # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
# utilisé: acceptable de tout invalider):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
# news # 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( sco_news.add(
typ=sco_news.NEWS_FORM, typ=sco_news.NEWS_FORM,
object=ue["formation_id"], object=ue["formation_id"],
@ -198,10 +222,10 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=ue["formation_id"], formation_id=ue["formation_id"],
semestre_idx=ue.semestre_idx,
) )
) )
else: return None
return None
def ue_create(formation_id=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): def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou création d'une UE""" """Modification ou création d'une UE"""
from app.scodoc import sco_formations
create = int(create) create = int(create)
if not create: if not create:
U = ue_list(args={"ue_id": ue_id}) 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): def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE""" """Delete an UE"""
ues = ue_list(args={"ue_id": ue_id}) ue = UniteEns.query.get_or_404(ue_id)
if not ues: if ue.modules.all():
raise ScoValueError("UE inexistante !") raise ScoValueError(
ue = ues[0] f"""Suppression de l'UE {ue.titre} impossible car
des modules (ou SAÉ ou ressources) lui sont rattachés."""
if not dialog_confirmed: )
return scu.confirm_dialog( if not can_delete_ue(ue):
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue, raise ScoNonEmptyFormationObject(
dest_url="", "UE",
parameters={"ue_id": ue_id}, msg=ue.titre,
cancel_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, 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 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) H.append(arrow_none)
im += 1 im += 1
if mod["nb_moduleimpls"] == 0 and editable: if mod["nb_moduleimpls"] == 0 and editable:
H.append( icon = delete_icon
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
)
else: else:
H.append(delete_disabled_icon) icon = delete_disabled_icon
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], icon)
)
H.append("</span>") H.append("</span>")
mod_editable = ( mod_editable = (

View File

@ -52,7 +52,7 @@ class InvalidNoteValue(ScoException):
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message # Exception qui stoque dest_url, utilisee dans Zope standard_error_message
class ScoValueError(ScoException): class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None): def __init__(self, msg, dest_url=None):
ScoException.__init__(self, msg) super().__init__(msg)
self.dest_url = dest_url self.dest_url = dest_url
@ -72,20 +72,35 @@ class ScoConfigurationError(ScoValueError):
pass pass
class ScoLockedFormError(ScoException): class ScoLockedFormError(ScoValueError):
def __init__(self, msg=""): "Modification d'une formation verrouillée"
def __init__(self, msg="", dest_url=None):
msg = ( msg = (
"Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). " "Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
+ str(msg) + 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): class ScoGenError(ScoException):
"exception avec affichage d'une page explicative ad-hoc" "exception avec affichage d'une page explicative ad-hoc"
def __init__(self, msg=""): def __init__(self, msg=""):
ScoException.__init__(self, msg) super().__init__(msg)
class AccessDenied(ScoGenError): class AccessDenied(ScoGenError):
@ -101,7 +116,7 @@ class APIInvalidParams(Exception):
status_code = 400 status_code = 400
def __init__(self, message, status_code=None, payload=None): def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self) super().__init__()
self.message = message self.message = message
if status_code is not None: if status_code is not None:
self.status_code = status_code self.status_code = status_code

View File

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

View File

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