workflows: prevent action replay (#86577) #1095
|
@ -1689,6 +1689,33 @@ def test_backoffice_handling(pub):
|
|||
assert 'HELLO WORLD' in resp.text
|
||||
|
||||
|
||||
def test_backoffice_parallel_handling(pub, freezer):
|
||||
create_user(pub)
|
||||
create_environment(pub)
|
||||
form_class = FormDef.get_by_urlname('form-title').data_class()
|
||||
number31 = [x for x in form_class.select() if x.data['1'] == 'FOO BAR 30'][0]
|
||||
app = login(get_app(pub))
|
||||
resp = app.get('/backoffice/management/form-title/')
|
||||
assert re.findall(r'<tbody.*\/tbody>', resp.text, re.DOTALL)[0].count('<tr') == 17
|
||||
|
||||
# open formdata twice
|
||||
resp2 = resp.click(href='%s/' % number31.id)
|
||||
resp3 = resp.click(href='%s/' % number31.id)
|
||||
|
||||
freezer.move_to(datetime.timedelta(seconds=10))
|
||||
|
||||
resp2.forms[0]['comment'] = 'HELLO WORLD'
|
||||
resp2 = resp2.forms[0].submit('button_accept')
|
||||
resp2 = resp2.follow()
|
||||
|
||||
resp3.forms[0]['comment'] = 'HELLO WORLD'
|
||||
resp3 = resp3.forms[0].submit('button_accept')
|
||||
assert resp3.pyquery('.global-errors summary').text() == 'Error: parallel execution.'
|
||||
assert (
|
||||
resp3.pyquery('.global-errors summary + p').text()
|
||||
== 'Another action has been performed on this form in the meantime and data may have been changed.'
|
||||
)
|
||||
|
||||
|
||||
def test_backoffice_handling_global_action(pub):
|
||||
create_user(pub)
|
||||
|
||||
|
|
|
@ -461,6 +461,7 @@ def test_frontoffice_workflow_form_with_impossible_condition(pub):
|
|||
|
||||
formdata = formdef.data_class()()
|
||||
formdata.user_id = user.id
|
||||
formdata.just_created()
|
||||
formdata.status = 'wf-new'
|
||||
formdata.store()
|
||||
|
||||
|
|
|
@ -420,6 +420,9 @@ class CardBackOfficeStatusPage(FormBackOfficeStatusPage):
|
|||
sidebar_recorded_by_agent_message = _(
|
||||
'The card has been recorded on %(date)s with the identifier %(identifier)s by %(agent)s.'
|
||||
)
|
||||
replay_detailed_message = _(
|
||||
'Another action has been performed on this card in the meantime and data may have been changed.'
|
||||
)
|
||||
|
||||
def should_fold_summary(self, mine, request_user):
|
||||
return False
|
||||
|
|
|
@ -36,7 +36,7 @@ from wcs.qommon.admin.texts import TextsDirectory
|
|||
from wcs.qommon.upload_storage import get_storage_object
|
||||
from wcs.utils import record_timings
|
||||
from wcs.wf.editable import EditableWorkflowStatusItem
|
||||
from wcs.workflows import RedisplayFormException
|
||||
from wcs.workflows import RedisplayFormException, ReplayException
|
||||
|
||||
from ..qommon import _, audit, errors, misc, template
|
||||
|
||||
|
@ -166,6 +166,10 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
history_templates = ['wcs/formdata_history.html']
|
||||
status_templates = ['wcs/formdata_status.html']
|
||||
|
||||
replay_detailed_message = _(
|
||||
'Another action has been performed on this form in the meantime and data may have been changed.'
|
||||
)
|
||||
|
||||
def __init__(self, formdef, filled, register_workflow_subdirs=True, custom_view=None, parent_view=None):
|
||||
get_publisher().substitutions.feed(filled)
|
||||
self.formdef = formdef
|
||||
|
@ -726,7 +730,18 @@ class FormStatusPage(Directory, FormTemplateMixin):
|
|||
|
||||
def submit(self, form):
|
||||
user = get_request().user
|
||||
next_url = self.filled.handle_workflow_form(user, form)
|
||||
next_url = None
|
||||
try:
|
||||
next_url = self.filled.handle_workflow_form(user, form)
|
||||
except ReplayException:
|
||||
raise RedisplayFormException(
|
||||
form=form,
|
||||
error={
|
||||
'summary': _('Error: parallel execution.'),
|
||||
'details': self.replay_detailed_message,
|
||||
},
|
||||
)
|
||||
|
||||
if next_url:
|
||||
return next_url
|
||||
if form.has_errors():
|
||||
|
|
|
@ -201,6 +201,10 @@ class RedisplayFormException(Exception):
|
|||
form.add_global_errors([error])
|
||||
|
||||
|
||||
class ReplayException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_role_dependencies(roles):
|
||||
for role_id in roles or []:
|
||||
if not role_id:
|
||||
|
@ -2349,6 +2353,7 @@ class SerieOfActionsMixin:
|
|||
def get_action_form(self, filled, user, displayed_fields=None):
|
||||
form = Form(enctype='multipart/form-data', use_tokens=False)
|
||||
form.attrs['id'] = 'wf-actions'
|
||||
form.add_hidden('_ts', str(time.mktime(filled.last_update_time)))
|
||||
fpeters
commented
On pose le timestamp dans le formulaire. On pose le timestamp dans le formulaire.
|
||||
for item in self.items:
|
||||
if not item.check_auth(filled, user):
|
||||
continue
|
||||
|
@ -2394,6 +2399,8 @@ class SerieOfActionsMixin:
|
|||
return messages
|
||||
|
||||
def handle_form(self, form, filled, user, evo):
|
||||
if form.get('_ts') != str(time.mktime(filled.last_update_time)):
|
||||
raise ReplayException()
|
||||
fpeters
commented
Et quand le formulaire est envoyé, on vérifie que le timestamp correspond toujours. Et quand le formulaire est envoyé, on vérifie que le timestamp correspond toujours.
|
||||
evo.time = time.localtime()
|
||||
evo.set_user(formdata=filled, user=user, check_submitter=get_request().is_in_frontoffice())
|
||||
if not filled.evolution:
|
||||
|
|
Loading…
Reference in New Issue
On se base sur le last_update_time pour détecter que le formdata a bougé et il est juste précis à la seconde, on avance donc dans le temps pour être sûr qu'une différente soit enregistrée.