From 0a97f53a164f3e0e2f0fd96bda0748361ac076e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Thu, 29 Sep 2016 10:19:49 +0200 Subject: [PATCH] backoffice: protect against overwriting of backoffice submissions (#13356) --- tests/test_backoffice_pages.py | 70 ++++++++++++++++++++++++++++++++++++++++++ wcs/backoffice/submission.py | 4 +++ wcs/forms/root.py | 24 +++++++++++++-- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py index 6f9f177..551093d 100644 --- a/tests/test_backoffice_pages.py +++ b/tests/test_backoffice_pages.py @@ -1150,6 +1150,76 @@ def test_backoffice_submission_welco(pub, welco_url): # check agent name is displayed next to pending submission assert '(%s)' % user.display_name in resp.body +def test_backoffice_parallel_submission(pub): + user = create_user(pub) + create_environment(pub) + + app = login(get_app(pub)) + resp = app.get('/backoffice/') + app.get('/backoffice/submission/', status=403) + + formdef = FormDef.get_by_urlname('form-title') + formdef.backoffice_submission_roles = user.roles[:] + formdef.enable_tracking_codes = True + formdef.store() + + formdata = formdef.data_class()() + formdata.data = {} + formdata.status = 'draft' + formdata.backoffice_submission = True + formdata.submission_context = {'agent_id': user.id} + formdata.store() + + resp = app.get('/backoffice/submission/') + assert 'Submission to complete' in resp.body + resp1 = app.get('/backoffice/submission/form-title/%s' % formdata.id) + resp1 = resp1.follow() + resp2 = app.get('/backoffice/submission/form-title/%s' % formdata.id) + resp2 = resp2.follow() + resp3 = app.get('/backoffice/submission/form-title/%s' % formdata.id) + resp3 = resp3.follow() + + resp1.form['f1'] = 'foo' + resp1.form['f2'] = 'bar' + resp1.form['f3'] = 'C' + resp1 = resp1.form.submit('submit') # to validation page + + # also move the second form to the validation page + resp2.form['f1'] = 'bar' + resp2.form['f2'] = 'bar' + resp2.form['f3'] = 'C' + resp2 = resp2.form.submit('submit') # to validation page + + resp1 = resp1.form.submit('submit') # final validation + resp1 = resp1.follow() + + resp2 = resp2.form.submit('submit') # final validation + assert resp2.status_code == 302 + resp2 = resp2.follow() + assert 'This form has already been submitted.' in resp2.body + + # do the third form from the start + resp3.form['f1'] = 'baz' + resp3.form['f2'] = 'bar' + resp3.form['f3'] = 'C' + + resp_autosave = app.post('/backoffice/submission/form-title/autosave', + params=resp3.form.submit_fields()) + assert resp_autosave.json['result'] == 'error' + assert resp_autosave.json['reason'] == 'form has already been submitted' + + resp3 = resp3.form.submit('submit') # to validation page + assert resp3.status_code == 302 + resp3 = resp3.follow() + assert 'This form has already been submitted.' in resp3.body + + assert formdef.data_class().get(formdata.id).data['1'] == 'foo' + + # try again, very late. + resp4 = app.get('/backoffice/submission/form-title/%s' % formdata.id) + resp4 = resp4.follow() + assert 'This form has already been submitted.' in resp4.body + def test_backoffice_submission_dispatch(pub): user = create_user(pub) create_environment(pub) diff --git a/wcs/backoffice/submission.py b/wcs/backoffice/submission.py index 1330125..05ed286 100644 --- a/wcs/backoffice/submission.py +++ b/wcs/backoffice/submission.py @@ -130,6 +130,9 @@ class FormFillPage(PublicFormFillPage): def submitted(self, form, *args): filled = self.get_current_draft() or self.formdef.data_class()() + if filled.id and filled.status != 'draft': + get_session().message = ('error', _('This form has already been submitted.')) + return redirect(get_publisher().get_backoffice_url() + '/submission/') filled.just_created() filled.data = self.formdef.get_data(form) filled.backoffice_submission = True @@ -209,6 +212,7 @@ class SubmissionDirectory(Directory): welco_url = get_publisher().get_site_option('welco_url', 'options') r = TemplateIO(html=True) + r += get_session().display_message() modes = ['empty', 'create', 'existing'] if welco_url: modes.remove('create') diff --git a/wcs/forms/root.py b/wcs/forms/root.py index 20d8878..f952818 100644 --- a/wcs/forms/root.py +++ b/wcs/forms/root.py @@ -56,6 +56,10 @@ from qommon.admin.texts import TextsDirectory from backoffice import FormDefUI +class SubmittedDraftException(Exception): + pass + + def html_top(title = None): template.html_top(title = title, default_org = _('Forms')) @@ -737,7 +741,13 @@ class FormPage(Directory): # if there's a draft (be it because drafts are enabled or # because the formdata was created as a draft via the # submission API), update it with current data. - self.autosave_draft(draft_id, page_no, form_data) + try: + self.autosave_draft(draft_id, page_no, form_data) + except SubmittedDraftException: + if get_request().is_in_backoffice(): + get_session().message = ('error', _('This form has already been submitted.')) + return redirect(get_publisher().get_backoffice_url() + '/submission/') + return template.error_page(_('This form has already been submitted.')) elif self.formdef.enable_tracking_codes and not self.edit_mode: # if there's no draft yet and tracking codes are enabled, create one filled = self.save_draft(form_data, page_no) @@ -856,7 +866,7 @@ class FormPage(Directory): return if not formdata.status == 'draft': - return + raise SubmittedDraftException() formdata.page_no = page_no formdata.data = form_data @@ -909,12 +919,17 @@ class FormPage(Directory): if not session.has_form_token(get_request().form.get('_ajax_form_token')): return result_error('obsolete ajax form token (late check)') - draft_formdata = self.save_draft(form_data, page_no) + try: + draft_formdata = self.save_draft(form_data, page_no) + except SubmittedDraftException: + return result_error('form has already been submitted') return json.dumps({'result': 'success'}) def save_draft(self, data, page_no): filled = self.get_current_draft() or self.formdef.data_class()() + if filled.id and filled.status != 'draft': + raise SubmittedDraftException() filled.data = data filled.status = 'draft' filled.page_no = page_no @@ -1113,6 +1128,9 @@ class FormPage(Directory): raise errors.TraversalError() if not filled.is_draft(): + if get_request().is_in_backoffice(): + get_session().message = ('error', _('This form has already been submitted.')) + return redirect(get_publisher().get_backoffice_url() + '/submission/') return PublicFormStatusPage(self.formdef, filled) if not (get_request().is_in_backoffice() or filled.formdef.enable_tracking_codes): -- 2.9.3