From 0c6e200412f1b4167a8c4db190aedd0d420ce7bb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sat, 1 Feb 2020 14:35:04 +0100
Subject: [PATCH] misc: load drafts from the form URL, with trailing slash
 (#39356)

---
 tests/test_api.py                       |  3 +-
 tests/test_backoffice_pages.py          | 32 +++++++-------
 tests/test_form_pages.py                | 49 ++++++++++------------
 wcs/api.py                              |  3 +-
 wcs/backoffice/management.py            |  2 +-
 wcs/backoffice/submission.py            | 19 ++++++++-
 wcs/forms/common.py                     | 42 +++++++++++++++++++
 wcs/forms/root.py                       | 56 ++++---------------------
 wcs/templates/wcs/formdata_filling.html |  4 +-
 wcs/wf/resubmit.py                      |  2 +-
 10 files changed, 111 insertions(+), 101 deletions(-)

diff --git a/tests/test_api.py b/tests/test_api.py
index cc1869295..1b2eb57c5 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1459,7 +1459,6 @@ def test_user_forms(pub, local_user):
     assert len(resp.json['data']) == 2
 
     draft_formdata = [x for x in resp.json['data'] if x['status'] == 'Draft'][0]
-    assert draft_formdata.get('url')[-1] != '/'
 
     formdata = formdef.data_class()()
     formdata.data = {'0': 'foo@localhost', '1': 'xyy'}
@@ -2595,7 +2594,7 @@ def test_tracking_code(pub):
 
     resp = get_app(pub).get(sign_url('/api/code/%s?orig=coucou' % code.id, '1234'), status=200)
     assert resp.json['err'] == 0
-    assert resp.json['url'] == 'http://example.net/test/%s' % formdata.id
+    assert resp.json['url'] == 'http://example.net/test/%s/' % formdata.id
     assert resp.json['load_url'] == 'http://example.net/code/%s/load' % code.id
 
     formdef.enable_tracking_codes = False
diff --git a/tests/test_backoffice_pages.py b/tests/test_backoffice_pages.py
index 825987961..d43269f7c 100644
--- a/tests/test_backoffice_pages.py
+++ b/tests/test_backoffice_pages.py
@@ -2072,7 +2072,7 @@ def test_backoffice_submission_initiated_from_welco(pub, welco_url):
         return resp.json['data']['id']
 
     resp = app.get('/backoffice/management/form-title/%s/' % post_formdata())
-    resp = resp.follow()  # -> /backoffice/submission/form-title/XXX
+    resp = resp.follow()  # -> /backoffice/submission/form-title/XXX/
     resp = resp.follow()  # -> /backoffice/submission/form-title/?mt=XYZ
 
     # second page should be shown
@@ -2085,7 +2085,7 @@ def test_backoffice_submission_initiated_from_welco(pub, welco_url):
     formdef.store()
 
     resp = app.get('/backoffice/management/form-title/%s/' % post_formdata())
-    resp = resp.follow()  # -> /backoffice/submission/form-title/XXX
+    resp = resp.follow()  # -> /backoffice/submission/form-title/XXX/
     resp = resp.follow()  # -> /backoffice/submission/form-title/?mt=XYZ
 
     pq = resp.pyquery.remove_namespaces()
@@ -2152,11 +2152,11 @@ def test_backoffice_parallel_submission(pub):
 
     resp = app.get('/backoffice/submission/')
     assert 'Submission to complete' in resp.text
-    resp1 = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp1 = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp1 = resp1.follow()
-    resp2 = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp2 = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp2 = resp2.follow()
-    resp3 = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp3 = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp3 = resp3.follow()
 
     resp1.form['f1'] = 'foo'
@@ -2196,7 +2196,7 @@ def test_backoffice_parallel_submission(pub):
     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 = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp4 = resp4.follow()
     assert 'This form has already been submitted.' in resp4.text
 
@@ -2343,7 +2343,7 @@ def test_backoffice_submission_drafts(pub):
 
     # check it can also be accessed using its final URL
     resp2 = app.get('/backoffice/management/%s/%s/' % (formdef.url_name, formdata_no))
-    assert resp2.location == 'http://example.net/backoffice/submission/%s/%s' % (
+    assert resp2.location == 'http://example.net/backoffice/submission/%s/%s/' % (
             formdef.url_name, formdata_no)
 
     resp = resp.click('#%s' % formdata_no)
@@ -2595,13 +2595,13 @@ def test_backoffice_submission_prefill_user(pub):
     assert resp.form['f1'].value == ''
 
     # restore a draft
-    resp = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp = resp.follow()
     # and check it got prefilled with the user from context
     assert resp.form['f1'].value == 'other@example.net'
 
     # restore another, without user id
-    resp = app.get('/backoffice/submission/form-title/%s' % formdata2.id)
+    resp = app.get('/backoffice/submission/form-title/%s/' % formdata2.id)
     resp = resp.follow()
     # and check it was not prefilled
     assert resp.form['f1'].value == ''
@@ -2642,13 +2642,13 @@ def test_backoffice_submission_prefill_user_via_formula(pub):
     assert resp.form['f1'].value == ''
 
     # restore a draft
-    resp = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp = resp.follow()
     # and check it got prefilled with the user from context
     assert resp.form['f1'].value == 'other@example.net'
 
     # restore another, without user id
-    resp = app.get('/backoffice/submission/form-title/%s' % formdata2.id)
+    resp = app.get('/backoffice/submission/form-title/%s/' % formdata2.id)
     resp = resp.follow()
     # and check it was not prefilled
     assert resp.form['f1'].value == ''
@@ -2700,14 +2700,14 @@ def test_backoffice_submission_prefill_user_multiple_pages(pub):
         assert resp.form['f5'].value == ''
 
         # restore a draft
-        resp = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+        resp = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
         resp = resp.follow()
         resp = resp.form.submit('submit')
         # and check it got prefilled with the user from context
         assert resp.form['f5'].value == 'other@example.net'
 
         # restore another, without user id
-        resp = app.get('/backoffice/submission/form-title/%s' % formdata2.id)
+        resp = app.get('/backoffice/submission/form-title/%s/' % formdata2.id)
         resp = resp.follow()
         resp = resp.form.submit('submit')
         # and check it was not prefilled
@@ -2719,7 +2719,7 @@ def test_backoffice_submission_prefill_user_multiple_pages(pub):
     formdata.page_no = 0
     formdata.user_id = other_user.id
     formdata.store()
-    resp = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp = resp.follow()
     resp.form['f1'] = 'Hello'
     resp_autosave = app.post('/backoffice/submission/form-title/autosave', params=resp.form.submit_fields())
@@ -2827,7 +2827,7 @@ def test_backoffice_submission_substitution_vars(pub):
     formdata.backoffice_submission = True
     formdata.store()
 
-    resp = app.get('/backoffice/submission/form-title/%s' % formdata.id)
+    resp = app.get('/backoffice/submission/form-title/%s/' % formdata.id)
     resp = resp.follow()
     resp.form['f1'].value = 'PLOP'
     resp.form['f10'].value = 'bar'
@@ -2901,7 +2901,7 @@ def test_backoffice_submission_with_nameid_and_channel(pub, local_user):
     resp = app.get('/backoffice/submission/form-title/?NameID=%s&channel=mail' % local_user.name_identifiers[0])
     assert resp.location.startswith('http://example.net/backoffice/submission/form-title/')
 
-    formdata_no = resp.location.split('/')[-1]
+    formdata_no = resp.location.split('/')[-2]
     formdata = formdef.data_class().get(formdata_no)
     assert formdata.user_id == str(local_user.id)
     assert formdata.submission_channel == 'mail'
diff --git a/tests/test_form_pages.py b/tests/test_form_pages.py
index 9e3b049b4..6e807cde2 100644
--- a/tests/test_form_pages.py
+++ b/tests/test_form_pages.py
@@ -175,7 +175,7 @@ def test_home_with_user_forms(pub):
     assert '<a href="test/%s"' % draft.id in resp
     resp = app.get('/test/%s' % formdata.id)
     resp.status_int = 200
-    resp = app.get('/test/%s' % draft.id, status=302)
+    resp = app.get('/test/%s/' % draft.id, status=302)
     resp = resp.follow(status=200)
 
     # disable formdef: formdatas are still visible and accessible, drafts are not
@@ -186,9 +186,8 @@ def test_home_with_user_forms(pub):
     assert '<a href="test/%s/"' % formdata.id in resp
     assert not 'Draft' in resp
     assert not '<a href="test/%s"' % draft.id in resp
-    resp = app.get('/test/%s' % formdata.id)
-    resp.status_int = 200
-    resp = app.get('/test/%s' % draft.id, status=302)
+    resp = app.get('/test/%s/' % formdata.id)
+    resp = app.get('/test/%s/' % draft.id, status=302)
     resp = resp.follow(status=403)
 
 
@@ -1478,20 +1477,20 @@ def test_form_tracking_code(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert resp.location.startswith('http://example.net/test/?mt=')
     resp = resp.follow()
 
     # check anonymous user can't get to it from the URL
     pub.session_manager.session_class.wipe()
-    resp = get_app(pub).get('http://example.net/test/%s' % formdata_id)
+    resp = get_app(pub).get('http://example.net/test/%s/' % formdata_id)
     assert resp.location.startswith('http://example.net/login')
 
     # or logged users that didn't enter the code:
     user = create_user(pub)
     login(get_app(pub), username='foo', password='foo').get(
-            'http://example.net/test/%s' % formdata_id, status=403)
+            'http://example.net/test/%s/' % formdata_id, status=403)
 
     # check we can also get to it as a logged user
     pub.session_manager.session_class.wipe()
@@ -1500,7 +1499,7 @@ def test_form_tracking_code(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code.lower()
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
 
     # go back as anonymous
@@ -1510,7 +1509,7 @@ def test_form_tracking_code(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert resp.location.startswith('http://example.net/test/?mt=')
     resp = resp.follow()
@@ -1536,8 +1535,7 @@ def test_form_tracking_code(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
-    resp = resp.follow()
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert 'form_comment' in resp.text # makes sure user is treated as submitter
     resp.forms[0]['comment'] = 'hello world'
@@ -1551,7 +1549,7 @@ def test_form_tracking_code(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code.lower()
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
 
 
@@ -1605,7 +1603,7 @@ def test_form_tracking_code_as_user(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert resp.location.startswith('http://example.net/test/?mt=')
     resp = resp.follow()
@@ -1631,8 +1629,7 @@ def test_form_tracking_code_as_user(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
-    resp = resp.follow()
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert 'form_comment' in resp.text # makes sure user is treated as submitter
     resp.forms[0]['comment'] = 'hello world'
@@ -1646,8 +1643,7 @@ def test_form_tracking_code_as_user(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
-    resp = resp.follow()
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert 'form_comment' in resp.text # makes sure user is treated as submitter
 
@@ -1740,7 +1736,7 @@ def test_form_tracking_code_remove_draft(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert resp.location.startswith('http://example.net/test/?mt=')
     resp = resp.follow()
@@ -1790,7 +1786,7 @@ def test_form_tracking_code_remove_empty_draft(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % tracking_code
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata_id
+    assert resp.location == 'http://example.net/test/%s/' % formdata_id
     resp = resp.follow()
     assert resp.location.startswith('http://example.net/test/?mt=')
     resp = resp.follow()
@@ -1933,7 +1929,7 @@ def test_form_invalid_tracking_code(pub, nocache):
     resp = resp.forms[0].submit()
     assert resp.location == 'http://example.net/code/%s/load' % code.id
     resp = resp.follow()
-    assert resp.location == 'http://example.net/test/%s' % formdata.id
+    assert resp.location == 'http://example.net/test/%s/' % formdata.id
     resp = resp.follow()
 
     # check we get a not found error message on non-existent code
@@ -2075,20 +2071,20 @@ def test_form_direct_draft_access(pub, tracking_code):
     formdata.status = 'draft'
     formdata.store()
 
-    resp = get_app(pub).get('/test/%s' % formdata.id, status=302)
+    resp = get_app(pub).get('/test/%s/' % formdata.id, status=302)
     assert resp.location.startswith('http://example.net/login')
 
     formdata.user_id = user.id
     formdata.store()
-    resp = get_app(pub).get('/test/%s' % formdata.id, status=302)
+    resp = get_app(pub).get('/test/%s/' % formdata.id, status=302)
     assert resp.location.startswith('http://example.net/login')
 
-    resp = login(get_app(pub), 'foo', 'foo').get('/test/%s' % formdata.id, status=302)
+    resp = login(get_app(pub), 'foo', 'foo').get('/test/%s/' % formdata.id, status=302)
     assert resp.location.startswith('http://example.net/test/?mt=')
 
     formdata.user_id = 1000
     formdata.store()
-    resp = login(get_app(pub), 'foo', 'foo').get('/test/%s' % formdata.id, status=403)
+    resp = login(get_app(pub), 'foo', 'foo').get('/test/%s/' % formdata.id, status=403)
 
 
 def form_password_field_submit(app, password):
@@ -6338,9 +6334,8 @@ def test_manager_public_access(pub):
     code.store()
 
     resp = app.get('/code/%s/load' % code.id)
-    resp = resp.follow() # -> /test/1
-    assert not 'backoffice' in resp.location
     resp = resp.follow() # -> /test/1/
+    assert 'backoffice' not in resp.request.path
     assert 'The form has been recorded' in resp.text
 
     # authorized access but not backoffice access
@@ -7171,7 +7166,7 @@ def test_form_recall_draft(pub):
     app = login(get_app(pub), username='foo', password='foo')
     resp = app.get('/test/')
     assert 'You already started to fill this form.' in resp.text
-    assert 'href="%s"' % draft.id in resp.text
+    assert 'href="%s/"' % draft.id in resp.text
 
     draft2 = formdef.data_class()()
     draft2.user_id = user.id
diff --git a/wcs/api.py b/wcs/api.py
index abc7dac1a..13e944deb 100644
--- a/wcs/api.py
+++ b/wcs/api.py
@@ -105,7 +105,6 @@ def get_formdata_dict(formdata, user, consider_status_visibility=True):
         d['last_update_time'] = misc.strftime('%Y-%m-%d %H:%M:%S', formdata.last_update_time)
 
     if formdata.is_draft():
-        d['url'] = d['url'].rstrip('/')
         d['form_number_raw'] = d['form_number'] = None
         d['title'] = _('%(name)s (draft)') % {'name': formdata.formdef.name}
     else:
@@ -790,7 +789,7 @@ class ApiTrackingCodeDirectory(Directory):
             raise TraversalError()
         data = {
             'err': 0,
-            'url': formdata.get_url().rstrip('/'),
+            'url': formdata.get_url(),
             'load_url': get_publisher().get_frontoffice_url() + '/code/%s/load' % component,
         }
         return json.dumps(data)
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 829fd29bd..1209996b3 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -2221,7 +2221,7 @@ class FormBackOfficeStatusPage(FormStatusPage):
             if self.filled.backoffice_submission:
                 for role in get_request().user.get_roles():
                     if role in self.formdef.backoffice_submission_roles:
-                        return redirect('../../../submission/%s/%s' % (
+                        return redirect('../../../submission/%s/%s/' % (
                             self.formdef.url_name, self.filled.id))
             raise errors.AccessForbiddenError()
 
diff --git a/wcs/backoffice/submission.py b/wcs/backoffice/submission.py
index 46bfebf36..011fdc066 100644
--- a/wcs/backoffice/submission.py
+++ b/wcs/backoffice/submission.py
@@ -30,6 +30,7 @@ from ..qommon import errors, misc
 from wcs.formdata import FormData
 from wcs.formdef import FormDef
 from wcs.categories import Category
+from wcs.forms.common import FormStatusPage
 from wcs.forms.root import FormPage as PublicFormFillPage
 
 
@@ -73,6 +74,16 @@ class RemoveDraftDirectory(Directory):
         return redirect('../..')
 
 
+class SubmissionFormStatusPage(FormStatusPage):
+    _q_exports_orig = ['', 'download', 'live']
+
+    def _q_index(self):
+        if not self.filled.is_draft():
+            get_session().message = ('error', _('This form has already been submitted.'))
+            return redirect(get_publisher().get_backoffice_url() + '/submission/')
+        return super(SubmissionFormStatusPage, self)._q_index()
+
+
 class FormFillPage(PublicFormFillPage):
     _q_exports = ['', 'tempfile', 'autosave', 'code',
             ('remove', 'remove_draft'), 'live']
@@ -108,7 +119,7 @@ class FormFillPage(PublicFormFillPage):
                 formdata.submission_context['return_url'] = return_url
             formdata.store()
             self.set_tracking_code(formdata)
-            return redirect('%s' % formdata.id)
+            return redirect('%s/' % formdata.id)
 
         self.selected_submission_channel = get_request().form.get('submission_channel') or ''
         return super(FormFillPage, self)._q_index(*args, **kwargs)
@@ -116,6 +127,10 @@ class FormFillPage(PublicFormFillPage):
     def html_top(self, *args, **kwargs):
         return html_top('submission', *args, **kwargs)
 
+    @classmethod
+    def get_status_page_class(cls):
+        return SubmissionFormStatusPage
+
     def check_authentication_context(self):
         pass
 
@@ -380,7 +395,7 @@ class SubmissionDirectory(Directory):
                             formdata.submission_context['agent_id'], ignore_errors=True)
                     if agent_user:
                         label += ' (%s)' % agent_user.display_name
-                r += htmltext('<a href="%s/%s">%s</a>') % (
+                r += htmltext('<a href="%s/%s/">%s</a>') % (
                         formdef.url_name, formdata.id, label)
                 r += htmltext('</li>')
 
diff --git a/wcs/forms/common.py b/wcs/forms/common.py
index 95a69728f..4f6f0a45c 100644
--- a/wcs/forms/common.py
+++ b/wcs/forms/common.py
@@ -19,6 +19,7 @@ import time
 from quixote import get_publisher, get_request, get_response, get_session, redirect
 from quixote.directory import Directory
 from quixote.html import TemplateIO, htmltext
+from quixote.util import randbytes
 
 from wcs import data_sources
 from wcs.api_utils import get_user_from_api_query_string, is_url_signed
@@ -226,6 +227,9 @@ class FormStatusPage(Directory, FormTemplateMixin):
         return r.getvalue()
 
     def _q_index(self):
+        if self.filled.is_draft():
+            return self.restore_draft()
+
         mine = self.check_auth()
         if not mine and not get_request().is_in_backoffice():
             # Access authorized but the form doesn't belong to the user; if the
@@ -259,6 +263,44 @@ class FormStatusPage(Directory, FormTemplateMixin):
                 templates=list(self.get_formdef_template_variants(self.status_templates)),
                 context=context)
 
+    def restore_draft(self):
+        # restore and redirect to draft
+        session = get_session()
+        filled = self.filled
+        if not (get_request().is_in_backoffice() and filled.backoffice_submission):
+            if session.is_anonymous_submitter(filled):
+                pass
+            elif session.user:
+                if str(session.user) != str(filled.user_id):
+                    raise errors.AccessUnauthorizedError()
+            else:
+                raise errors.AccessUnauthorizedError()
+
+        if get_request().get_query() == 'remove-draft':
+            filled.remove_self()
+            return redirect(get_publisher().get_root_url())
+
+        magictoken = randbytes(8)
+        filled.feed_session()
+        form_data = filled.data
+        for field in filled.formdef.fields:
+            if field.id not in form_data:
+                continue
+            if form_data[field.id] is None:
+                # remove keys that were not set, this is required when we restore a
+                # draft from SQL (where all columns are always defined).
+                del form_data[field.id]
+                continue
+            if field.type == 'file':
+                # add back file to session
+                tempfile = session.add_tempfile(form_data[field.id])
+                form_data[field.id].token = tempfile['token']
+        form_data['is_recalled_draft'] = True
+        form_data['draft_formdata_id'] = filled.id
+        form_data['page_no'] = filled.page_no
+        session.add_magictoken(magictoken, form_data)
+        return redirect('../?mt=%s' % magictoken)
+
     def get_workflow_form(self, user):
         submitted_fields = []
         form = self.filled.get_workflow_form(user, displayed_fields=submitted_fields)
diff --git a/wcs/forms/root.py b/wcs/forms/root.py
index 50fde5c39..d1e92776a 100644
--- a/wcs/forms/root.py
+++ b/wcs/forms/root.py
@@ -175,7 +175,7 @@ class TrackingCodeDirectory(Directory):
         if BotFilter.is_bot():
             raise errors.AccessForbiddenError()
         get_session().mark_anonymous_formdata(formdata)
-        return redirect(formdata.get_url().rstrip('/'))
+        return redirect(formdata.get_url())
 
 
 class TrackingCodesDirectory(Directory):
@@ -794,7 +794,7 @@ class FormPage(Directory, FormTemplateMixin):
 
             if self.has_draft_support() and form.get_submit() == 'savedraft':
                 filled = self.save_draft(form_data, page_no)
-                return redirect(filled.get_url().rstrip('/'))
+                return redirect(filled.get_url())
 
             for field in submitted_fields:
                 if not field.is_visible(form_data, self.formdef) and 'f%s' % field.id in form._names:
@@ -925,7 +925,7 @@ class FormPage(Directory, FormTemplateMixin):
 
             if self.has_draft_support() and form.get_submit() == 'savedraft':
                 filled = self.save_draft(form_data, page_no = -1)
-                return redirect(filled.get_url().rstrip('/'))
+                return redirect(filled.get_url())
 
             # so it gets FakeFileWidget in preview mode
             form = self.create_view_form(form_data,
@@ -1282,57 +1282,17 @@ class FormPage(Directory, FormTemplateMixin):
         get_response().set_content_type('image/png')
         return s.getvalue()
 
+    @classmethod
+    def get_status_page_class(cls):
+        return PublicFormStatusPage
+
     def _q_lookup(self, component):
         try:
             filled = self.formdef.data_class().get(component)
         except KeyError:
             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)
-
-        # restore draft
-        session = get_session()
-        if not (get_request().is_in_backoffice() and filled.backoffice_submission):
-            if session.is_anonymous_submitter(filled):
-                pass
-            elif session.user:
-                if str(session.user) != str(filled.user_id):
-                    raise errors.AccessUnauthorizedError()
-            else:
-                raise errors.AccessUnauthorizedError()
-
-        if get_request().get_query() == 'remove-draft':
-            filled.remove_self()
-            return redirect(get_publisher().get_root_url())
-
-        magictoken = randbytes(8)
-        filled.feed_session()
-        form_data = filled.data
-        for field in filled.formdef.fields:
-            if not field.id in form_data:
-                continue
-            if form_data[field.id] is None:
-                # remove keys that were not set, this is required when we restore a
-                # draft from SQL (where all columns are always defined).
-                del form_data[field.id]
-                continue
-            if field.type == 'file':
-                # add back file to session
-                tempfile = session.add_tempfile(form_data[field.id])
-                form_data[field.id].token = tempfile['token']
-        form_data['is_recalled_draft'] = True
-        form_data['draft_formdata_id'] = filled.id
-        form_data['page_no'] = filled.page_no
-        session.add_magictoken(magictoken, form_data)
-
-        if get_request().is_in_backoffice():
-            return redirect('./?mt=%s' % magictoken)
-        else:
-            return redirect('%s?mt=%s' % (filled.formdef.get_url(), magictoken))
+        return self.get_status_page_class()(self.formdef, filled)
 
 
 class RootDirectory(AccessControlled, Directory):
diff --git a/wcs/templates/wcs/formdata_filling.html b/wcs/templates/wcs/formdata_filling.html
index 8d8aa774c..e36489f30 100644
--- a/wcs/templates/wcs/formdata_filling.html
+++ b/wcs/templates/wcs/formdata_filling.html
@@ -22,11 +22,11 @@
        {% endblocktrans %}
      </p>
      {% if drafts_length == 1 %}
-       <p><a class="pk-button" href="{{drafts.0.internal_id}}">{% trans "Continue with draft" %}</a></p>
+       <p><a class="pk-button" href="{{drafts.0.internal_id}}/">{% trans "Continue with draft" %}</a></p>
      {% elif drafts_length > 1 %}
      <ul>
      {% for draft in drafts %}
-     <li><a href="{{draft.internal_id}}">{% trans "continue with draft from " %} {{draft.receipt_date}}
+     <li><a href="{{draft.internal_id}}/">{% trans "continue with draft from " %} {{draft.receipt_date}}
                      {{draft.receipt_time}}</a>, {% blocktrans with page_no=draft.page_no|add:1 %}on page {{page_no}}{% endblocktrans %}</li>
      {% endfor %}
      </ul>
diff --git a/wcs/wf/resubmit.py b/wcs/wf/resubmit.py
index 8e18381cb..012dbd051 100644
--- a/wcs/wf/resubmit.py
+++ b/wcs/wf/resubmit.py
@@ -106,7 +106,7 @@ class ResubmitWorkflowStatusItem(WorkflowStatusItem):
 
         workflow_data = {
             'resubmit_formdata_backoffice_url': new_formdata.get_url(backoffice=True),
-            'resubmit_formdata_draft_url': new_formdata.get_url(backoffice=False).rstrip('/'),
+            'resubmit_formdata_draft_url': new_formdata.get_url(backoffice=False),
         }
         formdata.update_workflow_data(workflow_data)
         formdata.store()
-- 
2.25.0

