From 8c0e83f0e9e4302cf94ca733cf11c82b62523f71 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 30 Jul 2020 15:15:44 +0200
Subject: [PATCH 1/2] misc: split form_pages tests

---
 tests/form_pages/test_all.py      | 1241 --------------------------
 tests/form_pages/test_block.py    |   17 +-
 tests/form_pages/test_formdata.py | 1358 +++++++++++++++++++++++++++++
 3 files changed, 1359 insertions(+), 1257 deletions(-)
 create mode 100644 tests/form_pages/test_formdata.py

diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index be338691..0e87413d 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -3,12 +3,10 @@
 import json
 import pytest
 import hashlib
-import locale
 import os
 import re
 import time
 import zipfile
-import base64
 from webtest import Upload, Hidden
 import mock
 import xml.etree.ElementTree as ET
@@ -21,12 +19,10 @@ except ImportError:
 from django.utils.six import StringIO, BytesIO
 from django.utils.six.moves.urllib import parse as urlparse
 
-from quixote.http_request import Upload as QuixoteUpload
 from django.utils.encoding import force_bytes, force_text
 
 from wcs.qommon import force_str
 from wcs.qommon.emails import docutils
-from wcs.qommon.form import UploadedFile
 from wcs.qommon.ident.password_accounts import PasswordAccount
 from wcs.carddef import CardDef
 from wcs.formdef import FormDef
@@ -36,9 +32,7 @@ from wcs.workflows import (
     ChoiceWorkflowStatusItem, JumpOnSubmitWorkflowStatusItem,
     CommentableWorkflowStatusItem, WorkflowVariablesFieldsFormDef)
 from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem
-from wcs.wf.export_to_model import ExportToModel, transform_to_pdf
 from wcs.wf.jump import JumpWorkflowStatusItem
-from wcs.wf.attachment import AddAttachmentWorkflowStatusItem
 from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
 from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
 from wcs.wf.resubmit import ResubmitWorkflowStatusItem
@@ -48,7 +42,6 @@ from wcs.categories import Category
 from wcs.roles import Role, logged_users_role
 from wcs.tracking_code import TrackingCode
 from wcs.data_sources import NamedDataSource
-from wcs.wscalls import NamedWsCall
 from wcs import fields
 from wcs.logged_errors import LoggedError
 from wcs.forms.root import PublicFormStatusPage
@@ -3524,1140 +3517,6 @@ def test_form_table_rows_add_row(pub):
     assert 'There were errors processing the form' in resp
 
 
-def test_formdata_attachment_download(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    attach = AddAttachmentWorkflowStatusItem()
-    attach.id = '_attach'
-    attach.by = ['_submitter']
-    st1.items.append(attach)
-    attach.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
-    resp = resp.forms[0].submit('button_attach')
-
-    assert formdef.data_class().count() == 1
-    formdata = formdef.data_class().select()[0]
-    assert formdata.evolution[-1].parts[0].__class__.__name__ == 'AttachmentEvolutionPart'
-    attachment = formdata.evolution[-1].parts[0]
-    assert attachment.content_type == 'text/plain'
-    assert attachment.orig_filename == 'test.txt'
-
-    resp = resp.follow() # back to form page
-    resp = resp.click('test.txt')
-    assert resp.location.endswith('/test.txt')
-    resp = resp.follow()
-    assert resp.content_type == 'text/plain'
-    assert resp.text == 'foobar'
-
-
-def test_formdata_attachment_download_with_substitution_variable(pub):
-    create_user_and_admin(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    attach = AddAttachmentWorkflowStatusItem()
-    attach.varname = 'attached_doc'
-    attach.id = '_attach'
-    attach.by = ['_submitter']
-    st1.items.append(attach)
-    attach.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
-    resp = resp.forms[0].submit('button_attach')
-
-    assert formdef.data_class().count() == 1
-    formdata = formdef.data_class().select()[0]
-    assert formdata.evolution[-1].parts[0].__class__.__name__ == 'AttachmentEvolutionPart'
-    attachment = formdata.evolution[-1].parts[0]
-    assert attachment.content_type == 'text/plain'
-    assert attachment.orig_filename == 'test.txt'
-
-    resp = resp.follow() # back to form page
-    resp = resp.click('test.txt')
-    assert resp.location.endswith('/test.txt')
-    resp = resp.follow()
-    assert resp.content_type == 'text/plain'
-    assert resp.text == 'foobar'
-
-    variables = formdef.data_class().select()[0].get_substitution_variables()
-    assert 'attachments' in variables
-    attachments = variables['attachments']
-    assert attachments is not None
-    attachment_variable = attachments.attached_doc
-
-    resp = login(get_app(pub), username='admin', password='admin').get(
-        attachment_variable.url).follow()
-    assert attachment_variable.content == resp.body
-    assert attachment_variable.b64_content == base64.b64encode(resp.body)
-    assert attachment_variable.content_type == resp._headers['content-type'].split(';')[0]
-    content_disposition = resp._headers['content-disposition']
-    assert len(content_disposition.split(';')) == 2
-    assert content_disposition.split(';')[0] == 'attachment'
-    assert resp.request.environ['PATH_INFO'].endswith(attachment_variable.filename)
-
-
-def test_formdata_attachment_download_to_backoffice_file_field(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
-    wf.backoffice_fields_formdef.fields = [
-        fields.FileField(id='bo1', label='bo field 1', type='file'),
-    ]
-    st1 = wf.add_status('Status1', 'st1')
-    attach = AddAttachmentWorkflowStatusItem()
-    attach.id = '_attach'
-    attach.by = ['_submitter']
-    attach.backoffice_filefield_id = 'bo1'
-    st1.items.append(attach)
-    attach.parent = st1
-    wf.store()
-
-    assert attach.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
-    resp = resp.forms[0].submit('button_attach')
-
-    # backoffice file field is set
-    assert formdef.data_class().count() == 1
-    formdata = formdef.data_class().select()[0]
-    assert 'bo1' in formdata.data
-    bo1 = formdata.data['bo1']
-    assert bo1.base_filename == 'test.txt'
-    assert bo1.content_type == 'text/plain'
-    assert bo1.get_content() == b'foobar'
-
-    # and file is in history, too
-    assert formdata.evolution[-1].parts[0].__class__.__name__ == 'AttachmentEvolutionPart'
-    attachment = formdata.evolution[-1].parts[0]
-    assert attachment.content_type == 'text/plain'
-    assert attachment.orig_filename == 'test.txt'
-
-
-def test_formdata_attachment_download_to_backoffice_file_field_only(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
-    wf.backoffice_fields_formdef.fields = [
-        fields.FileField(id='bo1', label='bo field 1', type='file'),
-    ]
-    st1 = wf.add_status('Status1', 'st1')
-    attach = AddAttachmentWorkflowStatusItem()
-    attach.id = '_attach'
-    attach.by = ['_submitter']
-    attach.backoffice_filefield_id = 'bo1'
-    attach.attach_to_history = False  # store only in backoffice field
-    st1.items.append(attach)
-    attach.parent = st1
-    wf.store()
-
-    assert attach.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
-    resp = resp.forms[0].submit('button_attach')
-
-    # backoffice file field is set
-    assert formdef.data_class().count() == 1
-    formdata = formdef.data_class().select()[0]
-    assert 'bo1' in formdata.data
-    bo1 = formdata.data['bo1']
-    assert bo1.base_filename == 'test.txt'
-    assert bo1.content_type == 'text/plain'
-    assert bo1.get_content() == b'foobar'
-
-    # but nothing in history
-    for evo in formdata.evolution:
-        assert not evo.parts
-
-
-def test_formdata_attachment_file_options(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    attach = AddAttachmentWorkflowStatusItem()
-    attach.id = '_attach'
-    attach.by = ['_submitter']
-    attach.document_type = {'label': 'Fichiers vidéo', 'mimetypes': ['video/*'], 'id': '_video'}
-    attach.max_file_size = '3Mo'
-
-    st1.items.append(attach)
-    attach.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    file_input = resp.forms[0]['attachment_attach$file']
-    assert file_input.attrs['accept'] == 'video/*'
-    assert file_input.attrs['data-max-file-size'] == '3000000'
-
-
-def test_formdata_generated_document_download(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.convert_to_pdf = False
-    export_to.label = 'create doc'
-    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
-    upload.fp = BytesIO()
-    upload.fp.write(b'HELLO WORLD')
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.by = ['_submitter']
-    st1.items.append(export_to)
-    export_to.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp = resp.form.submit('button_export_to')
-
-    resp = resp.follow() # $form/$id/create_doc
-    resp = resp.follow() # $form/$id/create_doc/
-    assert resp.text == 'HELLO WORLD'
-
-    export_to.attach_to_history = True
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    resp = resp.click('test.rtf')
-    assert resp.location.endswith('/test.rtf')
-    resp = resp.follow()
-    assert resp.content_type == 'application/rtf'
-    assert resp.text == 'HELLO WORLD'
-
-    # change export model to now be a RTF file, do the action again on the same form and
-    # check that both the old .odt file and the new .rtf file are there and valid.
-    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
-    upload.fp = BytesIO()
-    upload.fp.write(b'HELLO NEW WORLD')
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    assert resp.click('test.rtf', index=0).follow().text == 'HELLO WORLD'
-    assert resp.click('test.rtf', index=1).follow().text == 'HELLO NEW WORLD'
-
-    # use substitution variables on rtf: only ezt format is accepted
-    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
-    upload.fp = BytesIO()
-    upload.fp.write(b'HELLO {{DJANGO}} WORLD [form_name]')
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow()
-
-    assert resp.click('test.rtf', index=2).follow().text == 'HELLO {{DJANGO}} WORLD {\\uc1{test}}'
-
-
-@pytest.fixture(params=['template.odt', 'template-django.odt'])
-def odt_template(request):
-    return request.param
-
-
-def test_formdata_generated_document_odt_download(pub, odt_template):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.convert_to_pdf = False
-    export_to.label = 'create doc'
-    template_filename = os.path.join(os.path.dirname(__file__), '..', odt_template)
-    template = open(template_filename, 'rb').read()
-    upload = QuixoteUpload('/foo/' + odt_template, content_type='application/octet-stream')
-    upload.fp = BytesIO()
-    upload.fp.write(template)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.by = ['_submitter']
-    st1.items.append(export_to)
-    export_to.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp.form['f0'] = 'Hello\n\nWorld.'
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp = resp.form.submit('button_export_to')
-
-    resp = resp.follow() # $form/$id/create_doc
-    resp = resp.follow() # $form/$id/create_doc/
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        assert_equal_zip(BytesIO(resp.body), f)
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    resp = resp.follow() # $form/$id/create_doc
-    with mock.patch('wcs.wf.export_to_model.get_formdata_template_context') as get_context_1:
-        with mock.patch('wcs.workflows.get_formdata_template_context') as get_context_never:
-            get_context_1.return_value = {}
-            get_context_never.return_value = {}
-            resp = resp.follow() # $form/$id/create_doc/
-            # substitution variables are computed only one :
-            assert get_context_1.call_count == 1
-            assert get_context_never.call_count == 0
-
-    export_to.attach_to_history = True
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    resp = resp.click(odt_template)
-    assert resp.location.endswith('/' + odt_template)
-    resp = resp.follow()
-    assert resp.content_type == 'application/octet-stream'
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        assert_equal_zip(BytesIO(resp.body), f)
-
-    # change file content, same name
-    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
-    upload.fp = BytesIO()
-    upload.fp.write(b'HELLO NEW WORLD')
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        body = resp.click(odt_template, index=0).follow().body
-        assert_equal_zip(BytesIO(body), f)
-    assert resp.click('test.rtf', index=0).follow().body == b'HELLO NEW WORLD'
-
-
-def test_formdata_generated_document_odt_download_with_substitution_variable(pub):
-    create_user_and_admin(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.convert_to_pdf = False
-    export_to.label = 'create doc'
-    export_to.varname = 'created_doc'
-    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
-    template = open(template_filename, 'rb').read()
-    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
-    upload.fp = BytesIO()
-    upload.fp.write(template)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.by = ['_submitter']
-    st1.items.append(export_to)
-    export_to.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp.form['f0'] = 'Hello\n\nWorld.'
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp = resp.form.submit('button_export_to')
-
-    resp = resp.follow() # $form/$id/create_doc
-    resp = resp.follow() # $form/$id/create_doc/
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        assert_equal_zip(BytesIO(resp.body), f)
-
-    export_to.attach_to_history = True
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    resp = resp.click('template.odt')
-    assert resp.location.endswith('/template.odt')
-    response1 = resp = resp.follow()
-    assert resp.content_type == 'application/octet-stream'
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        assert_equal_zip(BytesIO(resp.body), f)
-
-    # change file content, same name
-    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
-    upload.fp = BytesIO()
-    upload.fp.write(b'HELLO NEW WORLD')
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        body = resp.click('template.odt', index=0).follow().body
-        assert_equal_zip(BytesIO(body), f)
-    response2 = resp.click('test.rtf', index=0).follow()
-    assert response2.body == b'HELLO NEW WORLD'
-    # Test attachment substitution variables
-    variables = formdef.data_class().select()[0].get_substitution_variables()
-    assert 'attachments' in variables
-    attachments = variables['attachments']
-    assert attachments is not None
-    file1 = attachments.created_doc
-    assert file1.content == response2.body
-    assert file1.b64_content == base64.b64encode(response2.body)
-    assert file1.content_type == response2._headers['content-type']
-    content_disposition = response2._headers['content-disposition']
-    assert len(content_disposition.split(';')) == 2
-    assert content_disposition.split(';')[0] == 'attachment'
-    assert response2.request.environ['PATH_INFO'].endswith(file1.filename)
-
-    resp = login(get_app(pub), username='admin', password='admin').get(file1.url).follow()
-    assert file1.content == resp.body
-    assert file1.b64_content == base64.b64encode(resp.body)
-    assert file1.content_type == resp._headers['content-type']
-    content_disposition = resp._headers['content-disposition']
-    assert len(content_disposition.split(';')) == 2
-    assert content_disposition.split(';')[0] == 'attachment'
-    assert resp.request.environ['PATH_INFO'].endswith(file1.filename)
-
-    file2 = attachments.created_doc[1]
-    assert file2.content == response1.body
-    assert file2.b64_content == base64.b64encode(response1.body)
-    assert file2.content_type == response1._headers['content-type']
-    content_disposition = response1._headers['content-disposition']
-    assert len(content_disposition.split(';')) == 2
-    assert content_disposition.split(';')[0] == 'attachment'
-    assert response1.request.environ['PATH_INFO'].endswith(file2.filename)
-
-    resp = login(get_app(pub), username='admin', password='admin').get(file2.url).follow()
-    assert file2.content == resp.body
-    assert file2.b64_content == base64.b64encode(resp.body)
-    assert file2.content_type == resp._headers['content-type']
-    content_disposition = resp._headers['content-disposition']
-    assert len(content_disposition.split(';')) == 2
-    assert content_disposition.split(';')[0] == 'attachment'
-    assert resp.request.environ['PATH_INFO'].endswith(file2.filename)
-
-
-@pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found')
-def test_formdata_generated_document_odt_to_pdf_download(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.label = 'create doc'
-    export_to.varname = 'created_doc'
-    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
-    template = open(template_filename, 'rb').read()
-    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
-    upload.fp = BytesIO()
-    upload.fp.write(template)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.by = ['_submitter']
-    export_to.convert_to_pdf = True
-    st1.items.append(export_to)
-    export_to.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp = resp.form.submit('button_export_to')
-
-    resp = resp.follow() # $form/$id/create_doc
-    resp = resp.follow() # $form/$id/create_doc/
-    assert resp.content_type == 'application/pdf'
-    assert b'PDF' in resp.body
-
-    export_to.attach_to_history = True
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    resp = resp.form.submit('button_export_to')
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow() # back to form page
-
-    resp = resp.click('template.pdf')
-    assert resp.location.endswith('/template.pdf')
-    resp = resp.follow()
-    assert resp.content_type == 'application/pdf'
-    content_disposition = resp._headers['content-disposition']
-    assert len(content_disposition.split(';')) == 2
-    assert content_disposition.split(';')[0] == 'inline'
-    assert resp.body.startswith(b'%PDF-')
-
-
-@pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found')
-def test_formdata_generated_document_odt_to_pdf_download_push_to_portfolio(pub, fargo_url,
-                                                                           fargo_secret, caplog):
-    user = create_user(pub)
-    user.name = 'Foo Baré'
-    user.store()
-
-    pub.cfg['debug'] = {'logger': True}
-    pub.write_cfg()
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.label = 'create doc'
-    export_to.varname = 'created_doc'
-    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
-    template = open(template_filename, 'rb').read()
-    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
-    upload.fp = BytesIO()
-    upload.fp.write(template)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.by = ['_submitter']
-    export_to.convert_to_pdf = True
-    export_to.push_to_portfolio = True
-    st1.items.append(export_to)
-    export_to.parent = st1
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    with mock.patch('wcs.portfolio.http_post_request') as http_post_request:
-        http_post_request.return_value = None, 200, 'null', None
-        resp = resp.form.submit('button_export_to')
-        assert http_post_request.call_count == 1
-        if locale.getpreferredencoding() == 'UTF-8':
-            assert ("file 'template.pdf' pushed to portfolio of 'Foo Baré'"
-                    == caplog.records[-1].message)
-        else:  # Python < 3.7
-            assert ("file 'template.pdf' pushed to portfolio of 'Foo Bar\xe9'"
-                    == caplog.records[-1].message)
-
-    resp = resp.follow()  # $form/$id/create_doc
-    resp = resp.follow()  # $form/$id/create_doc/
-    assert resp.content_type == 'application/pdf'
-    assert b'PDF' in resp.body
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    with mock.patch('wcs.portfolio.http_post_request') as http_post_request:
-        http_post_request.return_value = None, 400, 'null', None  # fail
-        resp = resp.form.submit('button_export_to')
-        assert http_post_request.call_count == 1
-        assert caplog.records[-1].message.startswith("file 'template.pdf' failed to be pushed to portfolio of 'Foo")
-
-    # failed to push to portfolio, but document is here
-    resp = resp.follow()  # $form/$id/create_doc
-    resp = resp.follow()  # $form/$id/create_doc/
-    assert resp.content_type == 'application/pdf'
-    assert b'PDF' in resp.body
-
-    export_to.attach_to_history = True
-    wf.store()
-
-    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
-    with mock.patch('wcs.portfolio.http_post_request') as http_post_request:
-        http_post_request.return_value = None, 200, 'null', None
-        resp = resp.form.submit('button_export_to')
-        assert http_post_request.call_count == 1
-        assert http_post_request.call_args[0][0].startswith('http://fargo.example.net/api/documents/push/')
-        payload = json.loads(http_post_request.call_args[0][1])
-        assert payload['file_name'] == 'template.pdf'
-        assert payload['user_email'] == 'foo@localhost'
-        assert payload['origin'] == 'example.net'
-        assert base64.decodebytes(force_bytes(payload['file_b64_content'])).startswith(b'%PDF')
-        assert caplog.records[-1].message.startswith("file 'template.pdf' pushed to portfolio of 'Foo")
-    assert resp.location == form_location + '#action-zone'
-    resp = resp.follow()  # back to form page
-
-    resp = resp.click('template.pdf')
-    assert resp.location.endswith('/template.pdf')
-    resp = resp.follow()
-    assert resp.content_type == 'application/pdf'
-    assert resp.body.startswith(b'%PDF-')
-
-
-def test_formdata_generated_document_non_interactive(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.convert_to_pdf = False
-    export_to.method = 'non-interactive'
-    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
-    template = open(template_filename, 'rb').read()
-    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
-    upload.fp = BytesIO()
-    upload.fp.write(template)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.attach_to_history = True
-    st1.items.append(export_to)
-    export_to.parent = st1
-
-    jump = JumpWorkflowStatusItem()
-    jump.status = 'st2'
-    st1.items.append(jump)
-    jump.parent = st1
-
-    st2 = wf.add_status('Status2', 'st2')
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp.form['f0'] = 'Hello\n\nWorld.'
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp = resp.click('template.odt')
-    assert resp.location.endswith('/template.odt')
-    resp = resp.follow()
-    assert resp.content_type == 'application/octet-stream'
-    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-        assert_equal_zip(BytesIO(resp.body), f)
-
-    assert formdef.data_class().count() == 1
-    assert formdef.data_class().select()[0].status == 'wf-st2'
-
-
-def test_formdata_generated_document_to_backoffice_field(pub):
-    create_user_and_admin(pub)
-    wf = Workflow(name='status')
-    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
-    wf.backoffice_fields_formdef.fields = [
-        fields.FileField(id='bo1', label='bo field 1', type='file'),
-        fields.StringField(id='bo2', label='bo field 2', type='string'),
-    ]
-
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.convert_to_pdf = False
-    export_to.method = 'non-interactive'
-    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
-    template = open(template_filename, 'rb').read()
-    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
-    upload.fp = BytesIO()
-    upload.fp.write(template)
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.id = '_export_to'
-    export_to.attach_to_history = True
-    export_to.backoffice_filefield_id = 'bo1'
-    st1.items.append(export_to)
-    export_to.parent = st1
-
-    assert export_to.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
-
-    jump = JumpWorkflowStatusItem()
-    jump.status = 'st2'
-    st1.items.append(jump)
-    jump.parent = st1
-    wf.add_status('Status2', 'st2')
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp.form['f0'] = 'Hello\n\nWorld.'
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    # get the two generated files from backoffice: in backoffice fields
-    # (export_to.backoffice_filefield_id), and in history (export_to.attach_to_history)
-    for index in (0, 1):
-        resp = login(get_app(pub), username='admin', password='admin').get(
-                '/backoffice/management/test/1/')
-        resp = resp.click('template.odt', index=index)
-        assert resp.location.endswith('/template.odt')
-        resp = resp.follow()
-        assert resp.content_type == 'application/octet-stream'
-        with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
-            assert_equal_zip(BytesIO(resp.body), f)
-
-    assert formdef.data_class().count() == 1
-    assert formdef.data_class().select()[0].status == 'wf-st2'
-
-
-def test_formdata_generated_document_in_private_history(pub):
-    user = create_user(pub)
-
-    Role.wipe()
-    role = Role(name='xxx')
-    role.store()
-
-    user.roles = [role.id]
-    user.store()
-
-    wf = Workflow(name='status')
-    st0 = wf.add_status('Status0', 'st0')
-    st1 = wf.add_status('Status1', 'st1')
-    export_to = ExportToModel()
-    export_to.label = 'create doc'
-    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
-    upload.fp = BytesIO()
-    upload.fp.write(b'HELLO WORLD')
-    upload.fp.seek(0)
-    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
-    export_to.attach_to_history = True
-    export_to.id = '_export_to'
-    export_to.by = ['_submitter']
-    st1.items.append(export_to)
-    export_to.parent = st1
-
-    st2 = wf.add_status('Status2', 'st2')
-
-    jump1 = ChoiceWorkflowStatusItem()
-    jump1.id = '_jump1'
-    jump1.label = 'Jump 1'
-    jump1.by = ['_receiver']
-    jump1.status = st1.id
-    jump1.parent = st0
-    st0.items.append(jump1)
-
-    jump2 = ChoiceWorkflowStatusItem()
-    jump2.id = '_jump2'
-    jump2.label = 'Jump 2'
-    jump2.by = ['_receiver']
-    jump2.status = st2.id
-    jump2.parent = st1
-    st1.items.append(jump2)
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.workflow_roles = {'_receiver': role.id}
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    form_location = resp.location
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    resp = resp.form.submit('button_jump1')
-    resp = resp.follow()
-
-    resp = resp.form.submit('button_export_to')
-    resp = resp.follow()
-    assert 'Form exported in a model' in resp.text
-
-    resp = resp.form.submit('button_jump2')
-    resp = resp.follow()
-
-    # limit visibility of status with document
-    st1.visibility = ['_receiver']
-    wf.store()
-
-    formdata = formdef.data_class().select()[0]
-    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url())
-    assert not 'Form exported in a model' in resp.text
-
-    # check status is visible in backoffice
-    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url(backoffice=True))
-    assert 'visibility-off' in resp.text
-    assert 'Form exported in a model' in resp.text
-
-
-def test_formdata_form_file_download(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-
-    display_form = FormWorkflowStatusItem()
-    display_form.id = '_x'
-    display_form.by = ['_submitter']
-    display_form.varname = 'xxx'
-    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
-    display_form.formdef.fields.append(fields.FileField(id='1', label='File',
-        type='file', varname='yyy'))
-    st1.items.append(display_form)
-    display_form.parent = st1
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-
-    assert 'qommon.fileupload.js' in resp.text
-    resp.forms[0]['f1$file'] = Upload('test.txt', b'foobar', 'text/plain')
-    resp = resp.forms[0].submit('submit')
-
-    assert formdef.data_class().count() == 1
-    formdata = formdef.data_class().select()[0]
-    assert 'xxx_var_yyy_raw' in formdata.workflow_data
-
-    download = resp.test_app.get(urlparse.urljoin(resp.location, 'files/form-xxx-yyy/test.txt'))
-    assert download.content_type == 'text/plain'
-    assert download.body == b'foobar'
-
-    # go back to the status page, this will exercise the substitution variables
-    # codepath.
-    resp = resp.follow()
-
-
-def test_formdata_workflow_form_prefill(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-
-    display_form = FormWorkflowStatusItem()
-    display_form.id = '_x'
-    display_form.by = ['_submitter']
-    display_form.varname = 'xxx'
-    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
-    display_form.formdef.fields.append(fields.StringField(id='1', label='blah',
-        type='string', varname='yyy', prefill={'type': 'user', 'value': 'email'}))
-    st1.items.append(display_form)
-    display_form.parent = st1
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-    assert resp.forms[0]['f1'].value == 'foo@localhost'
-
-
-def test_formdata_workflow_form_prefill_conditional_field(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-
-    display_form = FormWorkflowStatusItem()
-    display_form.id = '_x'
-    display_form.by = ['_submitter']
-    display_form.varname = 'xxx'
-    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
-    display_form.formdef.fields.append(fields.StringField(id='1', label='blah1',
-        type='string', varname='yyy1', prefill={'type': 'user', 'value': 'email'},
-        condition={'type': 'django', 'value': '0'}))
-    display_form.formdef.fields.append(fields.StringField(id='2', label='blah2',
-        type='string', varname='yyy2', prefill={'type': 'user', 'value': 'email'},
-        condition={'type': 'django', 'value': '1'}))
-    st1.items.append(display_form)
-    display_form.parent = st1
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded' in resp.text
-    assert resp.forms[0]['f2'].value == 'foo@localhost'
-
-
-def test_formdata_workflow_form_prefill_checkbox(pub):
-    create_user(pub)
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-
-    display_form = FormWorkflowStatusItem()
-    display_form.id = '_x'
-    display_form.by = ['_submitter']
-    display_form.varname = 'xxx'
-    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
-    display_form.formdef.fields.append(fields.BoolField(id='1', label='blah',
-        type='bool', varname='yyy', prefill={'type': 'formula', 'value': 'True'}))
-    display_form.formdef.fields.append(fields.BoolField(id='2', label='blah2',
-        type='bool', varname='zzz', prefill={'type': 'formula', 'value': 'True'}))
-    st1.items.append(display_form)
-    display_form.parent = st1
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.form.submit('submit')
-    assert 'Check values then click submit.' in resp
-    resp = resp.form.submit('submit').follow()
-    assert 'The form has been recorded' in resp
-    assert resp.form['f1'].checked is True
-    assert resp.form['f2'].checked is True
-    resp.form['f1'].checked = False
-    resp = resp.form.submit('submit')
-
-    formdata = formdef.data_class().select()[0]
-    assert formdata.workflow_data['xxx_var_yyy_raw'] is False
-    assert formdata.workflow_data['xxx_var_zzz_raw'] is True
-
-
-def test_formdata_workflow_form_prefill_autocomplete(pub):
-    create_user(pub)
-
-    NamedDataSource.wipe()
-    data_source = NamedDataSource(name='foobar')
-    data_source.data_source = {'type': 'json', 'value': 'http://local-mock/test'}
-    data_source.query_parameter = 'q'
-    data_source.id_parameter = 'id'
-    data_source.store()
-
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-
-    display_form = FormWorkflowStatusItem()
-    display_form.id = '_x'
-    display_form.by = ['_submitter']
-    display_form.varname = 'xxx'
-    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
-    display_form.formdef.fields = [
-        fields.ItemField(id='4', label='string', type='item',
-            data_source={'type': 'foobar'},
-            required=False,
-            display_mode='autocomplete',
-            prefill={'type': 'string', 'value': '{{ form_var_foo_raw }}'},
-        ),
-    ]
-    st1.items.append(display_form)
-    display_form.parent = st1
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = [
-        fields.ItemField(id='0', label='string', type='item',
-            data_source={'type': 'foobar'},
-            display_mode='autocomplete',
-            required=False,
-            varname='foo',
-        ),
-    ]
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
-        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
-
-        def side_effect(url, *args):
-            return StringIO(json.dumps(data))
-
-        urlopen.side_effect = side_effect
-
-        assert 'data-select2-url=' in resp
-        # simulate select2
-        resp.form.fields['f0_display'] = Hidden(form=resp.form, tag='input', name='f0_display', pos=10)
-        resp.form['f0'].force_value('1')
-        resp.form.fields['f0_display'].force_value('foo')
-        resp = resp.form.submit('submit')
-        assert 'Check values then click submit.' in resp
-        resp = resp.form.submit('submit').follow()
-        assert 'The form has been recorded' in resp
-
-        # check display value is in form action widget
-        assert resp.form['f4'].attrs['data-value'] == '1'
-        assert resp.form['f4'].attrs['data-initial-display-value'] == 'hello'
-
-        # check it is also displayed in a fresh session
-        resp = login(get_app(pub), username='foo', password='foo').get(resp.request.url)
-        assert resp.form['f4'].attrs['data-value'] == '1'
-        assert resp.form['f4'].attrs['data-initial-display-value'] == 'hello'
-
-
 def test_form_map_field_back_and_submit(pub):
     formdef = create_formdef()
     formdef.fields = [
@@ -6680,106 +5539,6 @@ def test_logged_errors(pub):
     assert len(LoggedError.get_ids_with_indexed_value('workflow_id', 'X')) == 0
 
 
-def test_formdata_named_wscall(http_requests, pub):
-    create_user(pub)
-    NamedWsCall.wipe()
-
-    wscall = NamedWsCall()
-    wscall.name = 'Hello world'
-    wscall.request = {'url': 'http://remote.example.net/json'}
-    wscall.store()
-    assert wscall.slug == 'hello_world'
-
-    wf = Workflow(name='status')
-    st1 = wf.add_status('Status1', 'st1')
-    comment = RegisterCommenterWorkflowStatusItem()
-    comment.id = '_comment'
-    comment.comment = 'Hello [webservice.hello_world.foo] World'
-    st1.items.append(comment)
-    comment.parent = st1
-
-    display = DisplayMessageWorkflowStatusItem()
-    display.message = 'The form has been recorded and: X[webservice.hello_world.foo]Y'
-    display.to = []
-    st1.items.append(display)
-    display.parent = st1
-
-    wf.store()
-
-    formdef = create_formdef()
-    formdef.workflow_id = wf.id
-    formdef.fields = []
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded and: XbarY' in resp.text
-
-    formdata = formdef.data_class().select()[0]
-    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
-
-    # check with publisher variable in named webservice call
-    if not pub.site_options.has_section('variables'):
-        pub.site_options.add_section('variables')
-    pub.site_options.set('variables', 'example_url', 'http://remote.example.net/')
-    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
-        pub.site_options.write(fd)
-
-    wscall = NamedWsCall()
-    wscall.name = 'Hello world'
-    wscall.request = {'url': '[example_url]json'}
-    wscall.store()
-
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    resp = resp.forms[0].submit('submit')
-    assert 'Check values then click submit.' in resp.text
-    resp = resp.forms[0].submit('submit')
-    assert resp.status_int == 302
-    resp = resp.follow()
-    assert 'The form has been recorded and: XbarY' in resp.text
-
-    formdata = formdef.data_class().select()[0]
-    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
-
-
-def test_formdata_named_wscall_in_conditions(http_requests, pub):
-    create_user(pub)
-    NamedWsCall.wipe()
-
-    wscall = NamedWsCall()
-    wscall.name = 'Hello world'
-    wscall.request = {'url': 'http://remote.example.net/json', 'method': 'GET'}
-    wscall.store()
-    assert wscall.slug == 'hello_world'
-
-    formdef = create_formdef()
-    formdef.fields = [
-        fields.PageField(id='0', label='1st page', type='page'),
-        fields.PageField(id='1', label='2nd page', type='page',
-            condition={'type': 'python', 'value': 'webservice.hello_world["foo"] == "bar"'}),
-        fields.PageField(id='1', label='3rd page', type='page',
-            condition={'type': 'python', 'value': 'webservice.hello_world["foo"] != "bar"'}),
-        fields.PageField(id='1', label='4th page', type='page',
-            condition={'type': 'python', 'value': 'webservice.hello_world["foo"] == "bar"'}),
-    ]
-    formdef.store()
-    formdef.data_class().wipe()
-
-    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
-    assert '>1st page<' in resp.text
-    assert '>2nd page<' in resp.text
-    assert '>3rd page<' not in resp.text
-    assert '>4th page<' in resp.text
-    assert len(http_requests.requests) == 1
-
-
 def test_resubmit(pub):
     user = create_user(pub)
 
diff --git a/tests/form_pages/test_block.py b/tests/form_pages/test_block.py
index 0b03a1d0..e9de3ed0 100644
--- a/tests/form_pages/test_block.py
+++ b/tests/form_pages/test_block.py
@@ -3,7 +3,6 @@
 import pytest
 from webtest import Upload
 
-from wcs.qommon.ident.password_accounts import PasswordAccount
 from wcs.blocks import BlockDef
 from wcs.formdef import FormDef
 from wcs.workflows import Workflow
@@ -13,6 +12,7 @@ from wcs.data_sources import NamedDataSource
 from wcs import fields
 
 from utilities import get_app, login, create_temporary_pub, clean_temporary_pub
+from .test_all import create_user
 
 
 def pytest_generate_tests(metafunc):
@@ -42,21 +42,6 @@ def teardown_module(module):
     clean_temporary_pub()
 
 
-def create_user(pub):
-    pub.user_class.wipe()
-    PasswordAccount.wipe()
-
-    user = pub.user_class()
-    user.name = 'User Name'
-    user.email = 'foo@localhost'
-    user.store()
-    account = PasswordAccount(id='foo')
-    account.set_password('foo')
-    account.user_id = user.id
-    account.store()
-    return user
-
-
 def test_block_simple(pub, blocks_feature):
     FormDef.wipe()
     BlockDef.wipe()
diff --git a/tests/form_pages/test_formdata.py b/tests/form_pages/test_formdata.py
new file mode 100644
index 00000000..33f7c85f
--- /dev/null
+++ b/tests/form_pages/test_formdata.py
@@ -0,0 +1,1358 @@
+# -*- coding: utf-8 -*-
+import base64
+import json
+import locale
+import os
+import xml.etree.ElementTree as ET
+import zipfile
+
+import mock
+import pytest
+from django.utils.encoding import force_bytes
+from django.utils.six import BytesIO, StringIO
+from django.utils.six.moves.urllib import parse as urlparse
+from quixote.http_request import Upload as QuixoteUpload
+from webtest import Upload, Hidden
+
+from wcs import fields
+from wcs.data_sources import NamedDataSource
+from wcs.formdef import FormDef
+from wcs.qommon.form import UploadedFile
+from wcs.roles import Role
+from wcs.wf.attachment import AddAttachmentWorkflowStatusItem
+from wcs.wf.export_to_model import ExportToModel, transform_to_pdf
+from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
+from wcs.wf.jump import JumpWorkflowStatusItem
+from wcs.wf.register_comment import RegisterCommenterWorkflowStatusItem
+from wcs.workflows import (ChoiceWorkflowStatusItem,
+                           DisplayMessageWorkflowStatusItem, Workflow,
+                           WorkflowBackofficeFieldsFormDef)
+from wcs.wscalls import NamedWsCall
+
+from .test_all import create_user, create_user_and_admin
+from utilities import clean_temporary_pub, create_temporary_pub, get_app, login
+
+
+def pytest_generate_tests(metafunc):
+    if 'pub' in metafunc.fixturenames:
+        metafunc.parametrize('pub', ['pickle', 'sql', 'pickle-templates', 'pickle-lazy'], indirect=True)
+
+
+@pytest.fixture
+def pub(request, emails):
+    pub = create_temporary_pub(
+            sql_mode=bool('sql' in request.param),
+            templates_mode=bool('templates' in request.param),
+            lazy_mode=bool('lazy' in request.param),
+            )
+    pub.cfg['identification'] = {'methods': ['password']}
+    pub.cfg['language'] = {'language': 'en'}
+    pub.write_cfg()
+
+    return pub
+
+
+def teardown_module(module):
+    clean_temporary_pub()
+
+
+def assert_equal_zip(stream1, stream2):
+    z1 = zipfile.ZipFile(stream1)
+    z2 = zipfile.ZipFile(stream2)
+    assert set(z1.namelist()) == set(z2.namelist())
+    for name in z1.namelist():
+        if name == 'styles.xml':
+            continue
+        if name in ['content.xml', 'meta.xml']:
+            t1, t2 = ET.tostring(ET.XML(z1.read(name))), ET.tostring(ET.XML(z2.read(name)))
+            try:
+                # >= python 3.8: tostring preserves attribute order; use canonicalize to sort them
+                t1, t2 = ET.canonicalize(t1), ET.canonicalize(t2)
+            except AttributeError:
+                pass
+        else:
+            t1, t2 = z1.read(name), z2.read(name)
+        assert t1 == t2, 'file "%s" differs' % name
+
+
+def test_formdata_attachment_download(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    attach = AddAttachmentWorkflowStatusItem()
+    attach.id = '_attach'
+    attach.by = ['_submitter']
+    st1.items.append(attach)
+    attach.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
+    resp = resp.forms[0].submit('button_attach')
+
+    assert formdef.data_class().count() == 1
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[-1].parts[0].__class__.__name__ == 'AttachmentEvolutionPart'
+    attachment = formdata.evolution[-1].parts[0]
+    assert attachment.content_type == 'text/plain'
+    assert attachment.orig_filename == 'test.txt'
+
+    resp = resp.follow()  # back to form page
+    resp = resp.click('test.txt')
+    assert resp.location.endswith('/test.txt')
+    resp = resp.follow()
+    assert resp.content_type == 'text/plain'
+    assert resp.text == 'foobar'
+
+
+def test_formdata_attachment_download_with_substitution_variable(pub):
+    create_user_and_admin(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    attach = AddAttachmentWorkflowStatusItem()
+    attach.varname = 'attached_doc'
+    attach.id = '_attach'
+    attach.by = ['_submitter']
+    st1.items.append(attach)
+    attach.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
+    resp = resp.forms[0].submit('button_attach')
+
+    assert formdef.data_class().count() == 1
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[-1].parts[0].__class__.__name__ == 'AttachmentEvolutionPart'
+    attachment = formdata.evolution[-1].parts[0]
+    assert attachment.content_type == 'text/plain'
+    assert attachment.orig_filename == 'test.txt'
+
+    resp = resp.follow()  # back to form page
+    resp = resp.click('test.txt')
+    assert resp.location.endswith('/test.txt')
+    resp = resp.follow()
+    assert resp.content_type == 'text/plain'
+    assert resp.text == 'foobar'
+
+    variables = formdef.data_class().select()[0].get_substitution_variables()
+    assert 'attachments' in variables
+    attachments = variables['attachments']
+    assert attachments is not None
+    attachment_variable = attachments.attached_doc
+
+    resp = login(get_app(pub), username='admin', password='admin').get(
+        attachment_variable.url).follow()
+    assert attachment_variable.content == resp.body
+    assert attachment_variable.b64_content == base64.b64encode(resp.body)
+    assert attachment_variable.content_type == resp._headers['content-type'].split(';')[0]
+    content_disposition = resp._headers['content-disposition']
+    assert len(content_disposition.split(';')) == 2
+    assert content_disposition.split(';')[0] == 'attachment'
+    assert resp.request.environ['PATH_INFO'].endswith(attachment_variable.filename)
+
+
+def test_formdata_attachment_download_to_backoffice_file_field(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
+    wf.backoffice_fields_formdef.fields = [
+        fields.FileField(id='bo1', label='bo field 1', type='file'),
+    ]
+    st1 = wf.add_status('Status1', 'st1')
+    attach = AddAttachmentWorkflowStatusItem()
+    attach.id = '_attach'
+    attach.by = ['_submitter']
+    attach.backoffice_filefield_id = 'bo1'
+    st1.items.append(attach)
+    attach.parent = st1
+    wf.store()
+
+    assert attach.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
+    resp = resp.forms[0].submit('button_attach')
+
+    # backoffice file field is set
+    assert formdef.data_class().count() == 1
+    formdata = formdef.data_class().select()[0]
+    assert 'bo1' in formdata.data
+    bo1 = formdata.data['bo1']
+    assert bo1.base_filename == 'test.txt'
+    assert bo1.content_type == 'text/plain'
+    assert bo1.get_content() == b'foobar'
+
+    # and file is in history, too
+    assert formdata.evolution[-1].parts[0].__class__.__name__ == 'AttachmentEvolutionPart'
+    attachment = formdata.evolution[-1].parts[0]
+    assert attachment.content_type == 'text/plain'
+    assert attachment.orig_filename == 'test.txt'
+
+
+def test_formdata_attachment_download_to_backoffice_file_field_only(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
+    wf.backoffice_fields_formdef.fields = [
+        fields.FileField(id='bo1', label='bo field 1', type='file'),
+    ]
+    st1 = wf.add_status('Status1', 'st1')
+    attach = AddAttachmentWorkflowStatusItem()
+    attach.id = '_attach'
+    attach.by = ['_submitter']
+    attach.backoffice_filefield_id = 'bo1'
+    attach.attach_to_history = False  # store only in backoffice field
+    st1.items.append(attach)
+    attach.parent = st1
+    wf.store()
+
+    assert attach.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp.forms[0]['attachment_attach$file'] = Upload('test.txt', b'foobar', 'text/plain')
+    resp = resp.forms[0].submit('button_attach')
+
+    # backoffice file field is set
+    assert formdef.data_class().count() == 1
+    formdata = formdef.data_class().select()[0]
+    assert 'bo1' in formdata.data
+    bo1 = formdata.data['bo1']
+    assert bo1.base_filename == 'test.txt'
+    assert bo1.content_type == 'text/plain'
+    assert bo1.get_content() == b'foobar'
+
+    # but nothing in history
+    for evo in formdata.evolution:
+        assert not evo.parts
+
+
+def test_formdata_attachment_file_options(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    attach = AddAttachmentWorkflowStatusItem()
+    attach.id = '_attach'
+    attach.by = ['_submitter']
+    attach.document_type = {'label': 'Fichiers vidéo', 'mimetypes': ['video/*'], 'id': '_video'}
+    attach.max_file_size = '3Mo'
+
+    st1.items.append(attach)
+    attach.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    file_input = resp.forms[0]['attachment_attach$file']
+    assert file_input.attrs['accept'] == 'video/*'
+    assert file_input.attrs['data-max-file-size'] == '3000000'
+
+
+def test_formdata_generated_document_download(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.convert_to_pdf = False
+    export_to.label = 'create doc'
+    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
+    upload.fp = BytesIO()
+    upload.fp.write(b'HELLO WORLD')
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.by = ['_submitter']
+    st1.items.append(export_to)
+    export_to.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    form_location = resp.location
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp = resp.form.submit('button_export_to')
+
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    assert resp.text == 'HELLO WORLD'
+
+    export_to.attach_to_history = True
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    resp = resp.click('test.rtf')
+    assert resp.location.endswith('/test.rtf')
+    resp = resp.follow()
+    assert resp.content_type == 'application/rtf'
+    assert resp.text == 'HELLO WORLD'
+
+    # change export model to now be a RTF file, do the action again on the same form and
+    # check that both the old .odt file and the new .rtf file are there and valid.
+    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
+    upload.fp = BytesIO()
+    upload.fp.write(b'HELLO NEW WORLD')
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    assert resp.click('test.rtf', index=0).follow().text == 'HELLO WORLD'
+    assert resp.click('test.rtf', index=1).follow().text == 'HELLO NEW WORLD'
+
+    # use substitution variables on rtf: only ezt format is accepted
+    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
+    upload.fp = BytesIO()
+    upload.fp.write(b'HELLO {{DJANGO}} WORLD [form_name]')
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()
+
+    assert resp.click('test.rtf', index=2).follow().text == 'HELLO {{DJANGO}} WORLD {\\uc1{test}}'
+
+
+@pytest.fixture(params=['template.odt', 'template-django.odt'])
+def odt_template(request):
+    return request.param
+
+
+def test_formdata_generated_document_odt_download(pub, odt_template):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.convert_to_pdf = False
+    export_to.label = 'create doc'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', odt_template)
+    template = open(template_filename, 'rb').read()
+    upload = QuixoteUpload('/foo/' + odt_template, content_type='application/octet-stream')
+    upload.fp = BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.by = ['_submitter']
+    st1.items.append(export_to)
+    export_to.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp.form['f0'] = 'Hello\n\nWorld.'
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    form_location = resp.location
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp = resp.form.submit('button_export_to')
+
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        assert_equal_zip(BytesIO(resp.body), f)
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    resp = resp.follow()  # $form/$id/create_doc
+    with mock.patch('wcs.wf.export_to_model.get_formdata_template_context') as get_context_1:
+        with mock.patch('wcs.workflows.get_formdata_template_context') as get_context_never:
+            get_context_1.return_value = {}
+            get_context_never.return_value = {}
+            resp = resp.follow()  # $form/$id/create_doc/
+            # substitution variables are computed only one :
+            assert get_context_1.call_count == 1
+            assert get_context_never.call_count == 0
+
+    export_to.attach_to_history = True
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    resp = resp.click(odt_template)
+    assert resp.location.endswith('/' + odt_template)
+    resp = resp.follow()
+    assert resp.content_type == 'application/octet-stream'
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        assert_equal_zip(BytesIO(resp.body), f)
+
+    # change file content, same name
+    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
+    upload.fp = BytesIO()
+    upload.fp.write(b'HELLO NEW WORLD')
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        body = resp.click(odt_template, index=0).follow().body
+        assert_equal_zip(BytesIO(body), f)
+    assert resp.click('test.rtf', index=0).follow().body == b'HELLO NEW WORLD'
+
+
+def test_formdata_generated_document_odt_download_with_substitution_variable(pub):
+    create_user_and_admin(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.convert_to_pdf = False
+    export_to.label = 'create doc'
+    export_to.varname = 'created_doc'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
+    template = open(template_filename, 'rb').read()
+    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
+    upload.fp = BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.by = ['_submitter']
+    st1.items.append(export_to)
+    export_to.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp.form['f0'] = 'Hello\n\nWorld.'
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    form_location = resp.location
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp = resp.form.submit('button_export_to')
+
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        assert_equal_zip(BytesIO(resp.body), f)
+
+    export_to.attach_to_history = True
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    resp = resp.click('template.odt')
+    assert resp.location.endswith('/template.odt')
+    response1 = resp = resp.follow()
+    assert resp.content_type == 'application/octet-stream'
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        assert_equal_zip(BytesIO(resp.body), f)
+
+    # change file content, same name
+    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
+    upload.fp = BytesIO()
+    upload.fp.write(b'HELLO NEW WORLD')
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        body = resp.click('template.odt', index=0).follow().body
+        assert_equal_zip(BytesIO(body), f)
+    response2 = resp.click('test.rtf', index=0).follow()
+    assert response2.body == b'HELLO NEW WORLD'
+    # Test attachment substitution variables
+    variables = formdef.data_class().select()[0].get_substitution_variables()
+    assert 'attachments' in variables
+    attachments = variables['attachments']
+    assert attachments is not None
+    file1 = attachments.created_doc
+    assert file1.content == response2.body
+    assert file1.b64_content == base64.b64encode(response2.body)
+    assert file1.content_type == response2._headers['content-type']
+    content_disposition = response2._headers['content-disposition']
+    assert len(content_disposition.split(';')) == 2
+    assert content_disposition.split(';')[0] == 'attachment'
+    assert response2.request.environ['PATH_INFO'].endswith(file1.filename)
+
+    resp = login(get_app(pub), username='admin', password='admin').get(file1.url).follow()
+    assert file1.content == resp.body
+    assert file1.b64_content == base64.b64encode(resp.body)
+    assert file1.content_type == resp._headers['content-type']
+    content_disposition = resp._headers['content-disposition']
+    assert len(content_disposition.split(';')) == 2
+    assert content_disposition.split(';')[0] == 'attachment'
+    assert resp.request.environ['PATH_INFO'].endswith(file1.filename)
+
+    file2 = attachments.created_doc[1]
+    assert file2.content == response1.body
+    assert file2.b64_content == base64.b64encode(response1.body)
+    assert file2.content_type == response1._headers['content-type']
+    content_disposition = response1._headers['content-disposition']
+    assert len(content_disposition.split(';')) == 2
+    assert content_disposition.split(';')[0] == 'attachment'
+    assert response1.request.environ['PATH_INFO'].endswith(file2.filename)
+
+    resp = login(get_app(pub), username='admin', password='admin').get(file2.url).follow()
+    assert file2.content == resp.body
+    assert file2.b64_content == base64.b64encode(resp.body)
+    assert file2.content_type == resp._headers['content-type']
+    content_disposition = resp._headers['content-disposition']
+    assert len(content_disposition.split(';')) == 2
+    assert content_disposition.split(';')[0] == 'attachment'
+    assert resp.request.environ['PATH_INFO'].endswith(file2.filename)
+
+
+@pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found')
+def test_formdata_generated_document_odt_to_pdf_download(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.label = 'create doc'
+    export_to.varname = 'created_doc'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
+    template = open(template_filename, 'rb').read()
+    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
+    upload.fp = BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.by = ['_submitter']
+    export_to.convert_to_pdf = True
+    st1.items.append(export_to)
+    export_to.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    form_location = resp.location
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp = resp.form.submit('button_export_to')
+
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    assert resp.content_type == 'application/pdf'
+    assert b'PDF' in resp.body
+
+    export_to.attach_to_history = True
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    resp = resp.form.submit('button_export_to')
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    resp = resp.click('template.pdf')
+    assert resp.location.endswith('/template.pdf')
+    resp = resp.follow()
+    assert resp.content_type == 'application/pdf'
+    content_disposition = resp._headers['content-disposition']
+    assert len(content_disposition.split(';')) == 2
+    assert content_disposition.split(';')[0] == 'inline'
+    assert resp.body.startswith(b'%PDF-')
+
+
+@pytest.mark.skipif(transform_to_pdf is None, reason='libreoffice not found')
+def test_formdata_generated_document_odt_to_pdf_download_push_to_portfolio(pub, fargo_url,
+                                                                           fargo_secret, caplog):
+    user = create_user(pub)
+    user.name = 'Foo Baré'
+    user.store()
+
+    pub.cfg['debug'] = {'logger': True}
+    pub.write_cfg()
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.label = 'create doc'
+    export_to.varname = 'created_doc'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
+    template = open(template_filename, 'rb').read()
+    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
+    upload.fp = BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.by = ['_submitter']
+    export_to.convert_to_pdf = True
+    export_to.push_to_portfolio = True
+    st1.items.append(export_to)
+    export_to.parent = st1
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    form_location = resp.location
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    with mock.patch('wcs.portfolio.http_post_request') as http_post_request:
+        http_post_request.return_value = None, 200, 'null', None
+        resp = resp.form.submit('button_export_to')
+        assert http_post_request.call_count == 1
+        if locale.getpreferredencoding() == 'UTF-8':
+            assert ("file 'template.pdf' pushed to portfolio of 'Foo Baré'"
+                    == caplog.records[-1].message)
+        else:  # Python < 3.7
+            assert ("file 'template.pdf' pushed to portfolio of 'Foo Bar\xe9'"
+                    == caplog.records[-1].message)
+
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    assert resp.content_type == 'application/pdf'
+    assert b'PDF' in resp.body
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    with mock.patch('wcs.portfolio.http_post_request') as http_post_request:
+        http_post_request.return_value = None, 400, 'null', None  # fail
+        resp = resp.form.submit('button_export_to')
+        assert http_post_request.call_count == 1
+        assert caplog.records[-1].message.startswith("file 'template.pdf' failed to be pushed to portfolio of 'Foo")
+
+    # failed to push to portfolio, but document is here
+    resp = resp.follow()  # $form/$id/create_doc
+    resp = resp.follow()  # $form/$id/create_doc/
+    assert resp.content_type == 'application/pdf'
+    assert b'PDF' in resp.body
+
+    export_to.attach_to_history = True
+    wf.store()
+
+    resp = login(get_app(pub), username='foo', password='foo').get(form_location)
+    with mock.patch('wcs.portfolio.http_post_request') as http_post_request:
+        http_post_request.return_value = None, 200, 'null', None
+        resp = resp.form.submit('button_export_to')
+        assert http_post_request.call_count == 1
+        assert http_post_request.call_args[0][0].startswith('http://fargo.example.net/api/documents/push/')
+        payload = json.loads(http_post_request.call_args[0][1])
+        assert payload['file_name'] == 'template.pdf'
+        assert payload['user_email'] == 'foo@localhost'
+        assert payload['origin'] == 'example.net'
+        assert base64.decodebytes(force_bytes(payload['file_b64_content'])).startswith(b'%PDF')
+        assert caplog.records[-1].message.startswith("file 'template.pdf' pushed to portfolio of 'Foo")
+    assert resp.location == form_location + '#action-zone'
+    resp = resp.follow()  # back to form page
+
+    resp = resp.click('template.pdf')
+    assert resp.location.endswith('/template.pdf')
+    resp = resp.follow()
+    assert resp.content_type == 'application/pdf'
+    assert resp.body.startswith(b'%PDF-')
+
+
+def test_formdata_generated_document_non_interactive(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.convert_to_pdf = False
+    export_to.method = 'non-interactive'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
+    template = open(template_filename, 'rb').read()
+    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
+    upload.fp = BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.attach_to_history = True
+    st1.items.append(export_to)
+    export_to.parent = st1
+
+    jump = JumpWorkflowStatusItem()
+    jump.status = 'st2'
+    st1.items.append(jump)
+    jump.parent = st1
+
+    wf.add_status('Status2', 'st2')
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp.form['f0'] = 'Hello\n\nWorld.'
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp = resp.click('template.odt')
+    assert resp.location.endswith('/template.odt')
+    resp = resp.follow()
+    assert resp.content_type == 'application/octet-stream'
+    with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+        assert_equal_zip(BytesIO(resp.body), f)
+
+    assert formdef.data_class().count() == 1
+    assert formdef.data_class().select()[0].status == 'wf-st2'
+
+
+def test_formdata_generated_document_to_backoffice_field(pub):
+    create_user_and_admin(pub)
+    wf = Workflow(name='status')
+    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
+    wf.backoffice_fields_formdef.fields = [
+        fields.FileField(id='bo1', label='bo field 1', type='file'),
+        fields.StringField(id='bo2', label='bo field 2', type='string'),
+    ]
+
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.convert_to_pdf = False
+    export_to.method = 'non-interactive'
+    template_filename = os.path.join(os.path.dirname(__file__), '..', 'template.odt')
+    template = open(template_filename, 'rb').read()
+    upload = QuixoteUpload('/foo/template.odt', content_type='application/octet-stream')
+    upload.fp = BytesIO()
+    upload.fp.write(template)
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.id = '_export_to'
+    export_to.attach_to_history = True
+    export_to.backoffice_filefield_id = 'bo1'
+    st1.items.append(export_to)
+    export_to.parent = st1
+
+    assert export_to.get_backoffice_filefield_options() == [('bo1', 'bo field 1', 'bo1')]
+
+    jump = JumpWorkflowStatusItem()
+    jump.status = 'st2'
+    st1.items.append(jump)
+    jump.parent = st1
+    wf.add_status('Status2', 'st2')
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = [fields.TextField(id='0', label='comment', type='text', varname='comment')]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp.form['f0'] = 'Hello\n\nWorld.'
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    # get the two generated files from backoffice: in backoffice fields
+    # (export_to.backoffice_filefield_id), and in history (export_to.attach_to_history)
+    for index in (0, 1):
+        resp = login(get_app(pub), username='admin', password='admin').get(
+                '/backoffice/management/test/1/')
+        resp = resp.click('template.odt', index=index)
+        assert resp.location.endswith('/template.odt')
+        resp = resp.follow()
+        assert resp.content_type == 'application/octet-stream'
+        with open(os.path.join(os.path.dirname(__file__), '..', 'template-out.odt'), 'rb') as f:
+            assert_equal_zip(BytesIO(resp.body), f)
+
+    assert formdef.data_class().count() == 1
+    assert formdef.data_class().select()[0].status == 'wf-st2'
+
+
+def test_formdata_generated_document_in_private_history(pub):
+    user = create_user(pub)
+
+    Role.wipe()
+    role = Role(name='xxx')
+    role.store()
+
+    user.roles = [role.id]
+    user.store()
+
+    wf = Workflow(name='status')
+    st0 = wf.add_status('Status0', 'st0')
+    st1 = wf.add_status('Status1', 'st1')
+    export_to = ExportToModel()
+    export_to.label = 'create doc'
+    upload = QuixoteUpload('/foo/test.rtf', content_type='application/rtf')
+    upload.fp = BytesIO()
+    upload.fp.write(b'HELLO WORLD')
+    upload.fp.seek(0)
+    export_to.model_file = UploadedFile(pub.app_dir, None, upload)
+    export_to.attach_to_history = True
+    export_to.id = '_export_to'
+    export_to.by = ['_submitter']
+    st1.items.append(export_to)
+    export_to.parent = st1
+
+    st2 = wf.add_status('Status2', 'st2')
+
+    jump1 = ChoiceWorkflowStatusItem()
+    jump1.id = '_jump1'
+    jump1.label = 'Jump 1'
+    jump1.by = ['_receiver']
+    jump1.status = st1.id
+    jump1.parent = st0
+    st0.items.append(jump1)
+
+    jump2 = ChoiceWorkflowStatusItem()
+    jump2.id = '_jump2'
+    jump2.label = 'Jump 2'
+    jump2.by = ['_receiver']
+    jump2.status = st2.id
+    jump2.parent = st1
+    st1.items.append(jump2)
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.workflow_roles = {'_receiver': role.id}
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    resp = resp.form.submit('button_jump1')
+    resp = resp.follow()
+
+    resp = resp.form.submit('button_export_to')
+    resp = resp.follow()
+    assert 'Form exported in a model' in resp.text
+
+    resp = resp.form.submit('button_jump2')
+    resp = resp.follow()
+
+    # limit visibility of status with document
+    st1.visibility = ['_receiver']
+    wf.store()
+
+    formdata = formdef.data_class().select()[0]
+    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url())
+    assert 'Form exported in a model' not in resp.text
+
+    # check status is visible in backoffice
+    resp = login(get_app(pub), username='foo', password='foo').get(formdata.get_url(backoffice=True))
+    assert 'visibility-off' in resp.text
+    assert 'Form exported in a model' in resp.text
+
+
+def test_formdata_form_file_download(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_x'
+    display_form.by = ['_submitter']
+    display_form.varname = 'xxx'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields.append(fields.FileField(
+        id='1', label='File',
+        type='file', varname='yyy'))
+    st1.items.append(display_form)
+    display_form.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+
+    assert 'qommon.fileupload.js' in resp.text
+    resp.forms[0]['f1$file'] = Upload('test.txt', b'foobar', 'text/plain')
+    resp = resp.forms[0].submit('submit')
+
+    assert formdef.data_class().count() == 1
+    formdata = formdef.data_class().select()[0]
+    assert 'xxx_var_yyy_raw' in formdata.workflow_data
+
+    download = resp.test_app.get(urlparse.urljoin(resp.location, 'files/form-xxx-yyy/test.txt'))
+    assert download.content_type == 'text/plain'
+    assert download.body == b'foobar'
+
+    # go back to the status page, this will exercise the substitution variables
+    # codepath.
+    resp = resp.follow()
+
+
+def test_formdata_workflow_form_prefill(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_x'
+    display_form.by = ['_submitter']
+    display_form.varname = 'xxx'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields.append(fields.StringField(
+        id='1', label='blah',
+        type='string', varname='yyy', prefill={'type': 'user', 'value': 'email'}))
+    st1.items.append(display_form)
+    display_form.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+    assert resp.forms[0]['f1'].value == 'foo@localhost'
+
+
+def test_formdata_workflow_form_prefill_conditional_field(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_x'
+    display_form.by = ['_submitter']
+    display_form.varname = 'xxx'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields.append(fields.StringField(
+        id='1', label='blah1',
+        type='string', varname='yyy1', prefill={'type': 'user', 'value': 'email'},
+        condition={'type': 'django', 'value': '0'}))
+    display_form.formdef.fields.append(fields.StringField(
+        id='2', label='blah2',
+        type='string', varname='yyy2', prefill={'type': 'user', 'value': 'email'},
+        condition={'type': 'django', 'value': '1'}))
+    st1.items.append(display_form)
+    display_form.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded' in resp.text
+    assert resp.forms[0]['f2'].value == 'foo@localhost'
+
+
+def test_formdata_workflow_form_prefill_checkbox(pub):
+    create_user(pub)
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_x'
+    display_form.by = ['_submitter']
+    display_form.varname = 'xxx'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields.append(fields.BoolField(
+        id='1', label='blah',
+        type='bool', varname='yyy', prefill={'type': 'formula', 'value': 'True'}))
+    display_form.formdef.fields.append(fields.BoolField(
+        id='2', label='blah2',
+        type='bool', varname='zzz', prefill={'type': 'formula', 'value': 'True'}))
+    st1.items.append(display_form)
+    display_form.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.form.submit('submit')
+    assert 'Check values then click submit.' in resp
+    resp = resp.form.submit('submit').follow()
+    assert 'The form has been recorded' in resp
+    assert resp.form['f1'].checked is True
+    assert resp.form['f2'].checked is True
+    resp.form['f1'].checked = False
+    resp = resp.form.submit('submit')
+
+    formdata = formdef.data_class().select()[0]
+    assert formdata.workflow_data['xxx_var_yyy_raw'] is False
+    assert formdata.workflow_data['xxx_var_zzz_raw'] is True
+
+
+def test_formdata_workflow_form_prefill_autocomplete(pub):
+    create_user(pub)
+
+    NamedDataSource.wipe()
+    data_source = NamedDataSource(name='foobar')
+    data_source.data_source = {'type': 'json', 'value': 'http://local-mock/test'}
+    data_source.query_parameter = 'q'
+    data_source.id_parameter = 'id'
+    data_source.store()
+
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+
+    display_form = FormWorkflowStatusItem()
+    display_form.id = '_x'
+    display_form.by = ['_submitter']
+    display_form.varname = 'xxx'
+    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
+    display_form.formdef.fields = [
+        fields.ItemField(
+            id='4', label='string', type='item',
+            data_source={'type': 'foobar'},
+            required=False,
+            display_mode='autocomplete',
+            prefill={'type': 'string', 'value': '{{ form_var_foo_raw }}'},
+        ),
+    ]
+    st1.items.append(display_form)
+    display_form.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = [
+        fields.ItemField(
+            id='0', label='string', type='item',
+            data_source={'type': 'foobar'},
+            display_mode='autocomplete',
+            required=False,
+            varname='foo',
+        ),
+    ]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    with mock.patch('wcs.qommon.misc.urlopen') as urlopen:
+        data = {'data': [{'id': '1', 'text': 'hello', 'extra': 'foo'}]}
+
+        def side_effect(url, *args):
+            return StringIO(json.dumps(data))
+
+        urlopen.side_effect = side_effect
+
+        assert 'data-select2-url=' in resp
+        # simulate select2
+        resp.form.fields['f0_display'] = Hidden(form=resp.form, tag='input', name='f0_display', pos=10)
+        resp.form['f0'].force_value('1')
+        resp.form.fields['f0_display'].force_value('foo')
+        resp = resp.form.submit('submit')
+        assert 'Check values then click submit.' in resp
+        resp = resp.form.submit('submit').follow()
+        assert 'The form has been recorded' in resp
+
+        # check display value is in form action widget
+        assert resp.form['f4'].attrs['data-value'] == '1'
+        assert resp.form['f4'].attrs['data-initial-display-value'] == 'hello'
+
+        # check it is also displayed in a fresh session
+        resp = login(get_app(pub), username='foo', password='foo').get(resp.request.url)
+        assert resp.form['f4'].attrs['data-value'] == '1'
+        assert resp.form['f4'].attrs['data-initial-display-value'] == 'hello'
+
+
+def test_formdata_named_wscall(http_requests, pub):
+    create_user(pub)
+    NamedWsCall.wipe()
+
+    wscall = NamedWsCall()
+    wscall.name = 'Hello world'
+    wscall.request = {'url': 'http://remote.example.net/json'}
+    wscall.store()
+    assert wscall.slug == 'hello_world'
+
+    wf = Workflow(name='status')
+    st1 = wf.add_status('Status1', 'st1')
+    comment = RegisterCommenterWorkflowStatusItem()
+    comment.id = '_comment'
+    comment.comment = 'Hello [webservice.hello_world.foo] World'
+    st1.items.append(comment)
+    comment.parent = st1
+
+    display = DisplayMessageWorkflowStatusItem()
+    display.message = 'The form has been recorded and: X[webservice.hello_world.foo]Y'
+    display.to = []
+    st1.items.append(display)
+    display.parent = st1
+
+    wf.store()
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.workflow_id = wf.id
+    formdef.fields = []
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded and: XbarY' in resp.text
+
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
+
+    # check with publisher variable in named webservice call
+    if not pub.site_options.has_section('variables'):
+        pub.site_options.add_section('variables')
+    pub.site_options.set('variables', 'example_url', 'http://remote.example.net/')
+    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
+        pub.site_options.write(fd)
+
+    wscall = NamedWsCall()
+    wscall.name = 'Hello world'
+    wscall.request = {'url': '[example_url]json'}
+    wscall.store()
+
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    resp = resp.forms[0].submit('submit')
+    assert 'Check values then click submit.' in resp.text
+    resp = resp.forms[0].submit('submit')
+    assert resp.status_int == 302
+    resp = resp.follow()
+    assert 'The form has been recorded and: XbarY' in resp.text
+
+    formdata = formdef.data_class().select()[0]
+    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
+
+
+def test_formdata_named_wscall_in_conditions(http_requests, pub):
+    create_user(pub)
+    NamedWsCall.wipe()
+
+    wscall = NamedWsCall()
+    wscall.name = 'Hello world'
+    wscall.request = {'url': 'http://remote.example.net/json', 'method': 'GET'}
+    wscall.store()
+    assert wscall.slug == 'hello_world'
+
+    FormDef.wipe()
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.fields = [
+        fields.PageField(id='0', label='1st page', type='page'),
+        fields.PageField(
+            id='1', label='2nd page', type='page',
+            condition={'type': 'python', 'value': 'webservice.hello_world["foo"] == "bar"'}),
+        fields.PageField(
+            id='1', label='3rd page', type='page',
+            condition={'type': 'python', 'value': 'webservice.hello_world["foo"] != "bar"'}),
+        fields.PageField(
+            id='1', label='4th page', type='page',
+            condition={'type': 'python', 'value': 'webservice.hello_world["foo"] == "bar"'}),
+    ]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = login(get_app(pub), username='foo', password='foo').get('/test/')
+    assert '>1st page<' in resp.text
+    assert '>2nd page<' in resp.text
+    assert '>3rd page<' not in resp.text
+    assert '>4th page<' in resp.text
+    assert len(http_requests.requests) == 1
-- 
2.28.0

