From 9db9a33db0a4997f29e6431965b69d261fa363df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 7 Sep 2021 10:28:57 +0200
Subject: [PATCH 2/3] misc: split workflow carddata tests

---
 tests/workflow/test_all.py      | 796 -----------------------------
 tests/workflow/test_carddata.py | 855 ++++++++++++++++++++++++++++++++
 2 files changed, 855 insertions(+), 796 deletions(-)
 create mode 100644 tests/workflow/test_carddata.py

diff --git a/tests/workflow/test_all.py b/tests/workflow/test_all.py
index 2e8b765a..36c0a669 100644
--- a/tests/workflow/test_all.py
+++ b/tests/workflow/test_all.py
@@ -59,7 +59,6 @@ from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
 from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
 from wcs.wf.criticality import MODE_DEC, MODE_INC, MODE_SET, ModifyCriticalityWorkflowStatusItem
 from wcs.wf.dispatch import DispatchWorkflowStatusItem
-from wcs.wf.edit_carddata import EditCarddataWorkflowStatusItem
 from wcs.wf.export_to_model import ExportToModel, transform_to_pdf
 from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
 from wcs.wf.geolocate import GeolocateWorkflowStatusItem
@@ -6249,327 +6248,6 @@ def test_create_formdata_attach_to_history(two_pubs):
     assert 'deleted' in str(formdata.evolution[-1].parts[0].view())
 
 
-def test_create_carddata(two_pubs):
-    CardDef.wipe()
-    FormDef.wipe()
-    if two_pubs.is_using_postgresql():
-        two_pubs.loggederror_class.wipe()
-
-    carddef = CardDef()
-    carddef.name = 'My card'
-    carddef.fields = [
-        StringField(id='1', label='string'),
-        ItemField(id='2', label='List', items=['item1', 'item2'], varname='clist'),
-        DateField(id='3', label='Date', varname='cdate'),
-        FileField(id='4', label='File', varname='cfile'),
-    ]
-    carddef.store()
-
-    wf = Workflow(name='create-carddata')
-    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
-    create = CreateCarddataWorkflowStatusItem()
-    create.label = 'Create CardDef'
-    create.varname = 'mycard'
-    create.id = '_create'
-    create.formdef_slug = carddef.url_name
-    create.mappings = [
-        Mapping(field_id='1', expression='=form_var_undefined'),
-        Mapping(field_id='2', expression='{{ form_var_list }}'),
-        Mapping(field_id='3', expression='{{ form_var_date }}'),
-        Mapping(field_id='4', expression='{{ form_var_file|default_if_none:"" }}'),
-    ]
-    create.parent = wf.possible_status[1]
-    wf.possible_status[1].items.insert(0, create)
-    wf.store()
-
-    formdef = FormDef()
-    formdef.name = 'source form'
-    formdef.fields = [
-        ItemField(id='1', label='List', items=['item1', 'item2'], varname='list'),
-        DateField(id='2', label='Date', varname='date'),
-        FileField(id='3', label='File', varname='file'),
-    ]
-    formdef.workflow_id = wf.id
-    formdef.store()
-
-    formdata = formdef.data_class()()
-    formdata.data = {}
-    formdata.just_created()
-
-    assert carddef.data_class().count() == 0
-
-    formdata = formdef.data_class()()
-    formdata.data = {}
-
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-
-    if two_pubs.is_using_postgresql():
-        errors = two_pubs.loggederror_class.select()
-        assert len(errors) == 2
-        assert any('form_var_undefined' in (error.exception_message or '') for error in errors)
-        assert any('invalid date value' in (error.exception_message or '') for error in errors)
-
-    formdata = formdef.data_class()()
-    today = datetime.date.today()
-
-    upload = PicklableUpload('test.jpeg', 'image/jpeg')
-    with open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb') as jpg:
-        upload.receive([jpg.read()])
-
-    formdata.data = {'1': 'item1', '1_display': 'item1', '2': today.timetuple(), '3': upload}
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert formdata.get_substitution_variables()['form_links_mycard_form_number'] == '1-2'
-    carddata = carddef.data_class().get(id=2)
-    assert carddata.data['2'] == 'item1'
-    assert carddata.data['2_display'] == 'item1'
-    assert carddata.data['3'] == today.timetuple()
-    assert carddata.data['4'].base_filename == 'test.jpeg'
-
-    create.condition = {'type': 'python', 'value': '1 == 2'}
-    wf.store()
-    del formdef._workflow
-    carddef.data_class().wipe()
-    assert carddef.data_class().count() == 0
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 0
-
-
-def test_create_carddata_with_map_field(two_pubs):
-    CardDef.wipe()
-    FormDef.wipe()
-    if two_pubs.is_using_postgresql():
-        two_pubs.loggederror_class.wipe()
-
-    carddef = CardDef()
-    carddef.name = 'My card'
-    carddef.fields = [
-        MapField(id='1', label='map', varname='map'),
-    ]
-    carddef.store()
-
-    wf = Workflow(name='create-carddata')
-    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
-    create = CreateCarddataWorkflowStatusItem()
-    create.label = 'Create CardDef'
-    create.varname = 'mycard'
-    create.id = '_create'
-    create.formdef_slug = carddef.url_name
-    create.mappings = [
-        Mapping(field_id='1', expression=''),
-    ]
-    create.parent = wf.possible_status[1]
-    wf.possible_status[1].items.insert(0, create)
-    wf.store()
-
-    formdef = FormDef()
-    formdef.name = 'source form'
-    formdef.fields = [
-        MapField(id='1', label='map', varname='map'),
-    ]
-    formdef.workflow_id = wf.id
-    formdef.store()
-
-    formdata = formdef.data_class()()
-    formdata.data = {}
-    formdata.just_created()
-    assert carddef.data_class().count() == 0
-
-    # empty value
-    formdata = formdef.data_class()()
-    formdata.data = {}
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 1
-    assert not carddef.data_class().select()[0].data.get('1')
-
-    # valid coordinates
-    create.mappings[0].expression = '1;2'
-    wf.store()
-    formdef.refresh_from_storage()
-    carddef.data_class().wipe()
-    formdata = formdef.data_class()()
-    formdata.data = {}
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].data.get('1') == '1;2'
-
-    # invalid value
-    create.mappings[0].expression = 'plop'
-    wf.store()
-    formdef.refresh_from_storage()
-    carddef.data_class().wipe()
-    formdata = formdef.data_class()()
-    formdata.data = {}
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 1
-    assert not carddef.data_class().select()[0].data.get('1')
-
-    if two_pubs.is_using_postgresql():
-        errors = two_pubs.loggederror_class.select()
-        assert len(errors) == 1
-        assert any('invalid coordinates' in (error.exception_message or '') for error in errors)
-
-    # value from formdata
-    create.mappings[0].expression = '{{ form_var_map }}'
-    wf.store()
-    formdef.refresh_from_storage()
-    carddef.data_class().wipe()
-    formdata = formdef.data_class()()
-    formdata.data = {'1': '2;3'}
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].data.get('1') == '2;3'
-
-
-def test_create_carddata_user_association(two_pubs):
-    CardDef.wipe()
-    FormDef.wipe()
-    two_pubs.user_class.wipe()
-
-    user = two_pubs.user_class()
-    user.email = 'test@example.net'
-    user.name_identifiers = ['xyz']
-    user.store()
-
-    carddef = CardDef()
-    carddef.name = 'My card'
-    carddef.fields = []
-    carddef.user_support = 'optional'
-    carddef.store()
-    carddef.data_class().wipe()
-
-    wf = Workflow(name='create-carddata')
-    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
-    create = CreateCarddataWorkflowStatusItem()
-    create.label = 'Create CardDef'
-    create.varname = 'mycard'
-    create.id = '_create'
-    create.formdef_slug = carddef.url_name
-    create.map_fields_by_varname = True
-    create.parent = wf.possible_status[1]
-    wf.possible_status[1].items.insert(0, create)
-    wf.store()
-
-    formdef = FormDef()
-    formdef.name = 'source form'
-    formdef.fields = []
-    formdef.workflow_id = wf.id
-    formdef.store()
-
-    formdata = formdef.data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user is None
-
-    # keep user
-    carddef.data_class().wipe()
-    create.user_association_mode = 'keep-user'
-    wf.store()
-
-    formdata = FormDef.get(formdef.id).data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user.id == user.id
-
-    # user association on direct user
-    carddef.data_class().wipe()
-    create.user_association_mode = 'custom'
-    create.user_association_template = '{{ form_user }}'
-    wf.store()
-
-    formdata = FormDef.get(formdef.id).data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user.id == user.id
-
-    # user association on user email
-    carddef.data_class().wipe()
-    create.user_association_mode = 'custom'
-    create.user_association_template = 'test@example.net'
-    wf.store()
-
-    formdata = FormDef.get(formdef.id).data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user.id == user.id
-
-    # user association on name id
-    carddef.data_class().wipe()
-    create.user_association_mode = 'custom'
-    create.user_association_template = 'xyz'
-    wf.store()
-
-    formdata = FormDef.get(formdef.id).data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user.id == user.id
-
-    # user association on invalid user
-    carddef.data_class().wipe()
-    create.user_association_mode = 'custom'
-    create.user_association_template = 'zzz'
-    wf.store()
-
-    formdata = FormDef.get(formdef.id).data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    if two_pubs.loggederror_class:
-        two_pubs.loggederror_class.wipe()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user is None
-    if two_pubs.loggederror_class:
-        assert two_pubs.loggederror_class.count() == 1
-        logged_error = two_pubs.loggederror_class.select()[0]
-        assert logged_error.summary == 'Failed to attach user (not found: "zzz")'
-        assert logged_error.formdata_id == str(carddef.data_class().select()[0].id)
-
-    # user association on invalid template
-    carddef.data_class().wipe()
-    create.user_association_mode = 'custom'
-    create.user_association_template = '{% %}'
-    wf.store()
-
-    formdata = FormDef.get(formdef.id).data_class()()
-    formdata.data = {}
-    formdata.user_id = user.id
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    assert carddef.data_class().select()[0].user is None
-
-
 def test_call_external_workflow_with_evolution_linked_object(two_pubs):
     FormDef.wipe()
     CardDef.wipe()
@@ -7089,477 +6767,3 @@ def test_call_external_workflow_manual_targeting(two_pubs):
     assert carddef.data_class().get(2).data['bo0'] is None
     assert carddef.data_class().get(3).data['bo0'] is None
     assert carddef.data_class().get(4).data['bo0'] is None
-
-
-def test_edit_carddata_with_data_sourced_object(pub):
-    FormDef.wipe()
-    CardDef.wipe()
-
-    datasource = {
-        'type': 'formula',
-        'value': repr(
-            [
-                {'id': 'b', 'text': 'baker', 'extra': 'plop'},
-                {'id': 'c', 'text': 'cook', 'extra': 'plop2'},
-                {'id': 'l', 'text': 'lawyer', 'extra': 'plop3'},
-            ]
-        ),
-    }
-    carddef = CardDef()
-    carddef.name = 'Person'
-    carddef.fields = [
-        StringField(id='0', label='First Name', varname='first_name'),
-        StringField(id='1', label='Last Name', varname='last_name'),
-        ItemField(id='2', label='Profession', type='item', varname='profession', data_source=datasource),
-    ]
-    carddef.digest_templates = {'default': '{{ form_var_first_name }} {{ form_var_last_name }}'}
-    carddef.store()
-
-    carddata = carddef.data_class()()
-    carddata.data = {'0': 'Foo', '1': 'Bar', '2': 'l'}
-    carddata.data['2_display'] = carddef.fields[2].store_display_value(carddata.data, '2')
-    carddata.data['2_structured'] = carddef.fields[2].store_structured_value(carddata.data, '2')
-    carddata.store()
-
-    wf = Workflow(name='Card update')
-    st1 = wf.add_status('Update card', 'st1')
-
-    edit = EditCarddataWorkflowStatusItem()
-    edit.formdef_slug = carddef.url_name
-    edit.mappings = [
-        Mapping(field_id='2', expression='{{ form_var_new_profession }}'),
-    ]
-    edit.id = 'edit'
-    st1.items.append(edit)
-    edit.parent = st1
-    wf.store()
-
-    datasource = {'type': 'carddef:%s' % carddef.url_name}
-    formdef = FormDef()
-    formdef.name = 'Persons'
-    formdef.fields = [
-        ItemField(id='0', label='Person', type='item', varname='person', data_source=datasource),
-        StringField(id='1', label='New profession', varname='new_profession'),
-    ]
-    formdef.workflow = wf
-    formdef.store()
-
-    formdata = formdef.data_class()()
-    formdata.data = {'0': '1', '1': 'c'}
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    data = carddef.data_class().select()[0]
-    assert data.data['2'] == 'c'
-    assert data.data['2_display'] == 'cook'
-    assert data.data['2_structured'] == {'id': 'c', 'text': 'cook', 'extra': 'plop2'}
-
-    formdata = formdef.data_class()()
-    formdata.data = {'0': '1', '1': 'b'}
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    data = carddef.data_class().select()[0]
-    assert data.data['2'] == 'b'
-    assert data.data['2_display'] == 'baker'
-    assert data.data['2_structured'] == {'id': 'b', 'text': 'baker', 'extra': 'plop'}
-
-    # reset data
-    for expression in ('=None', '', '""'):
-        edit.mappings = [
-            Mapping(field_id='2', expression=expression),
-        ]
-        wf.store()
-
-        formdata = formdef.data_class()()
-        formdata.data = {'0': '1', '1': 'b'}
-        formdata.store()
-        formdata.just_created()
-        formdata.perform_workflow()
-
-        carddata = carddef.data_class().select()[0]
-        assert carddata.data['2'] in [None, '', '""']
-        assert carddata.data.get('2_display') is None
-        assert carddata.data.get('2_structured') is None
-
-        # restore initial data
-        carddata.data = data.data
-        carddata.store()
-
-
-def test_edit_carddata_with_linked_object(pub):
-    FormDef.wipe()
-    CardDef.wipe()
-
-    carddef = CardDef()
-    carddef.name = 'Parent'
-    carddef.fields = [
-        StringField(id='0', label='First Name', varname='first_name'),
-        StringField(id='1', label='Last Name', varname='last_name'),
-        StringField(id='2', label='Kids number', varname='kids_number'),
-    ]
-    carddef.store()
-
-    wf = Workflow(name='Card create and update')
-    st1 = wf.add_status('Create card', 'st1')
-    edit = CreateCarddataWorkflowStatusItem()
-    edit.formdef_slug = carddef.url_name
-    edit.mappings = [
-        Mapping(field_id='0', expression='{{ form_var_first_name }}'),
-        Mapping(field_id='1', expression='{{ form_var_last_name }}'),
-        Mapping(field_id='2', expression='{{ form_var_kids_number|default:"0" }}'),
-    ]
-    st1.items.append(edit)
-    edit.parent = st1
-    jump = JumpWorkflowStatusItem()
-    jump.id = '_jump'
-    jump.by = ['_submitter', '_receiver']
-    jump.status = 'st2'
-    st1.items.append(jump)
-    jump.parent = st1
-
-    st2 = wf.add_status('Update card', 'st2')
-    edit = EditCarddataWorkflowStatusItem()
-    edit.formdef_slug = carddef.url_name
-    edit.mappings = [
-        Mapping(field_id='2', expression='{{ form_var_kids_number|add:"1" }}'),
-    ]
-    edit.id = 'edit'
-    st2.items.append(edit)
-    edit.parent = st2
-    wf.store()
-
-    formdef = FormDef()
-    formdef.name = 'Parents'
-    formdef.fields = [
-        StringField(id='0', label='First Name', varname='first_name'),
-        StringField(id='1', label='Last Name', varname='last_name'),
-        StringField(id='2', label='Number of kids', varname='kids_number'),
-    ]
-    formdef.workflow = wf
-    formdef.store()
-
-    formdata = formdef.data_class()()
-    formdata.data = {'0': 'Parent', '1': 'Foo', '2': '2'}
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 1
-    card_data = carddef.data_class().select()[0]
-    assert card_data.data['2'] == '3'
-
-
-def test_edit_carddata_manual_targeting(two_pubs):
-    if not two_pubs.is_using_postgresql():
-        pytest.skip('this requires SQL')
-        return
-
-    FormDef.wipe()
-    CardDef.wipe()
-    two_pubs.loggederror_class.wipe()
-
-    # carddef
-    carddef = CardDef()
-    carddef.name = 'Parent'
-    carddef.fields = [
-        StringField(id='0', label='First Name', varname='first_name'),
-        StringField(id='1', label='Last Name', varname='last_name'),
-        StringField(id='2', label='Kids number', varname='kids_number'),
-    ]
-    carddef.store()
-    carddef.data_class().wipe()
-
-    # and sample carddatas
-    for i in range(1, 4):
-        carddata = carddef.data_class()()
-        carddata.data = {
-            '0': 'First name %s' % i,
-            '1': 'Last name %s' % i,
-            '2': '0',
-        }
-        carddata.store()
-
-    # formdef workflow that will update carddata
-    wf = Workflow(name='Card create and update')
-    st1 = wf.add_status('Create card', 'st1')
-    # create linked carddata
-    edit = CreateCarddataWorkflowStatusItem()
-    edit.formdef_slug = carddef.url_name
-    edit.mappings = [
-        Mapping(field_id='0', expression='{{ form_var_first_name }}'),
-        Mapping(field_id='1', expression='{{ form_var_last_name }}'),
-        Mapping(field_id='2', expression='{{ form_var_kids_number|default:"0" }}'),
-    ]
-    st1.items.append(edit)
-    edit.parent = st1
-    jump = JumpWorkflowStatusItem()
-    jump.id = '_jump'
-    jump.by = ['_submitter', '_receiver']
-    jump.status = 'st2'
-    st1.items.append(jump)
-    jump.parent = st1
-
-    st2 = wf.add_status('Update card', 'st2')
-    edit = EditCarddataWorkflowStatusItem()
-    edit.formdef_slug = carddef.url_name
-    edit.target_mode = 'manual'  # not configured
-    edit.mappings = [
-        Mapping(field_id='2', expression='{{ form_var_kids_number|add:"1" }}'),
-    ]
-    edit.id = 'edit'
-    st2.items.append(edit)
-    edit.parent = st2
-    wf.store()
-
-    # associated formdef
-    formdef = FormDef()
-    formdef.name = 'Parents'
-    datasource = {'type': 'carddef:%s' % carddef.url_name}
-    formdef.fields = [
-        StringField(id='0', label='First Name', varname='first_name'),
-        StringField(id='1', label='Last Name', varname='last_name'),
-        StringField(id='2', label='Number of kids', varname='kids_number'),
-        ItemField(id='3', label='Card', type='item', varname='card', data_source=datasource),
-        StringField(id='4', label='string', varname='string'),
-    ]
-    formdef.workflow = wf
-    formdef.store()
-
-    # create formdatas
-
-    # target not configured
-    formdata = formdef.data_class()()
-    formdata.data = {
-        '0': 'Parent',
-        '1': 'Foo',
-        '2': '2',
-        '3': '3',  # set from datasource
-        '4': '1',
-    }
-    # set parent
-    formdata.submission_context = {
-        'orig_object_type': 'carddef',
-        'orig_formdata_id': '2',
-        'orig_formdef_id': str(carddef.id),
-    }
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-
-    assert carddef.data_class().count() == 4
-    assert carddef.data_class().get(1).data['2'] == '0'
-    assert carddef.data_class().get(2).data['2'] == '0'
-    assert carddef.data_class().get(3).data['2'] == '0'
-    assert carddef.data_class().get(4).data['2'] == '2'
-    assert two_pubs.loggederror_class.count() == 0
-
-    # configure target
-    edit.target_id = '{{ form_var_string }}'  # == '1'
-    wf.store()
-    formdata = formdef.data_class()()
-    formdata.data = {
-        '0': 'Parent',
-        '1': 'Foo',
-        '2': '2',
-        '3': '3',  # set from datasource
-        '4': '1',
-    }
-    # set parent
-    formdata.submission_context = {
-        'orig_object_type': 'carddef',
-        'orig_formdata_id': '2',
-        'orig_formdef_id': str(carddef.id),
-    }
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 5
-    assert carddef.data_class().get(1).data['2'] == '3'  # 2 + 1
-    assert carddef.data_class().get(2).data['2'] == '0'
-    assert carddef.data_class().get(3).data['2'] == '0'
-    assert carddef.data_class().get(4).data['2'] == '2'
-    assert carddef.data_class().get(5).data['2'] == '2'
-    assert two_pubs.loggederror_class.count() == 0
-
-    # target not found
-    edit.target_id = '42{{ form_var_string }}'  # == '421'
-    wf.store()
-    formdata = formdef.data_class()()
-    formdata.data = {
-        '0': 'Parent',
-        '1': 'Foo',
-        '2': '2',
-        '3': '3',  # set from datasource
-        '4': '1',
-    }
-    # set parent
-    formdata.submission_context = {
-        'orig_object_type': 'carddef',
-        'orig_formdata_id': '2',
-        'orig_formdef_id': str(carddef.id),
-    }
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 6
-    assert carddef.data_class().get(1).data['2'] == '3'  # not changed
-    assert carddef.data_class().get(2).data['2'] == '0'
-    assert carddef.data_class().get(3).data['2'] == '0'
-    assert carddef.data_class().get(4).data['2'] == '2'
-    assert carddef.data_class().get(5).data['2'] == '2'
-    assert carddef.data_class().get(6).data['2'] == '2'
-    assert two_pubs.loggederror_class.count() == 1
-    logged_error = two_pubs.loggederror_class.select()[0]
-    assert logged_error.summary == 'Could not find targeted "Parent" object by id 421'
-
-    # slug not or badly configured
-    edit.target_id = '{{ form_var_string }}'  # == '1'
-    edit.formdef_slug = None
-    wf.store()
-    formdata = formdef.data_class()()
-    formdata.data = {
-        '0': 'Parent',
-        '1': 'Foo',
-        '2': '3',
-        '3': '3',  # set from datasource
-        '4': '1',
-    }
-    # set parent
-    formdata.submission_context = {
-        'orig_object_type': 'carddef',
-        'orig_formdata_id': '2',
-        'orig_formdef_id': str(carddef.id),
-    }
-    formdata.store()
-    formdata.just_created()
-    formdata.perform_workflow()
-    assert carddef.data_class().count() == 7
-    assert carddef.data_class().get(1).data['2'] == '3'  # not changed
-    assert carddef.data_class().get(2).data['2'] == '0'
-    assert carddef.data_class().get(3).data['2'] == '0'
-    assert carddef.data_class().get(4).data['2'] == '2'
-    assert carddef.data_class().get(5).data['2'] == '2'
-    assert carddef.data_class().get(6).data['2'] == '2'
-    assert carddef.data_class().get(7).data['2'] == '3'
-
-
-def test_edit_carddata_targeting_itself(pub):
-    CardDef.wipe()
-    carddef = CardDef()
-    carddef.name = 'Foo Card'
-    carddef.fields = [
-        StringField(id='0', label='foo', varname='foo'),
-    ]
-    carddef.store()
-    carddef.data_class().wipe()
-
-    # card workflow: update itself then jump to second status
-    card_wf = Workflow(name='Card workflow')
-    st1 = card_wf.add_status('Status1')
-    st2 = card_wf.add_status('Status2')
-
-    edit = EditCarddataWorkflowStatusItem()
-    edit.id = '_edit'
-    edit.formdef_slug = carddef.url_name
-    edit.target_mode = 'manual'
-    edit.target_id = '{{ form_internal_id }}'  # itself
-    edit.mappings = [
-        Mapping(field_id='0', expression='bar {{ form_internal_id }}'),
-    ]
-    edit.parent = st1
-    st1.items.append(edit)
-
-    jump = JumpWorkflowStatusItem()
-    jump.id = '_jump'
-    jump.status = st2.id
-    st1.items.append(jump)
-    jump.parent = st1
-
-    card_wf.store()
-
-    carddef.workflow = card_wf
-    carddef.store()
-
-    # create some cardata
-    for i in range(1, 4):
-        carddata = carddef.data_class()()
-        carddata.data = {
-            '0': 'foo %s' % i,
-        }
-        carddata.store()
-        # run workflow, verify that carddata is modified
-        carddata.just_created()
-        carddata.perform_workflow()
-        assert carddata.data['0'] == 'bar %s' % carddata.id
-        assert carddata.status == 'wf-%s' % st2.id
-
-
-def test_edit_carddata_from_created_object(pub):
-    FormDef.wipe()
-    CardDef.wipe()
-
-    carddef = CardDef()
-    carddef.name = 'Card'
-    carddef.fields = [
-        StringField(id='0', label='Card Field', varname='card_field'),
-    ]
-    carddef.store()
-
-    formdef = FormDef()
-    formdef.name = 'Form'
-    formdef.fields = [
-        StringField(id='0', label='Form Field', varname='form_field'),
-    ]
-    formdef.store()
-
-    # card workflow: create formdata then jump to second status
-    card_wf = Workflow(name='Card workflow')
-    st1 = card_wf.add_status('Status1')
-    st2 = card_wf.add_status('Status2')
-
-    create = CreateFormdataWorkflowStatusItem()
-    create.id = '_create'
-    create.formdef_slug = formdef.url_name
-    create.mappings = [
-        Mapping(field_id='0', expression='...'),
-    ]
-    create.parent = st1
-    st1.items.append(create)
-
-    jump = JumpWorkflowStatusItem()
-    jump.id = '_jump'
-    jump.status = st2.id
-    st1.items.append(jump)
-    jump.parent = st1
-
-    # form workflow: edit parent card data
-    form_wf = Workflow(name='Form workflow')
-    st1 = form_wf.add_status('Status1')
-    edit = EditCarddataWorkflowStatusItem()
-    edit.formdef_slug = carddef.url_name
-    edit.mappings = [
-        Mapping(field_id='0', expression='HELLO'),
-    ]
-    edit.id = 'edit'
-    st1.items.append(edit)
-    edit.parent = st1
-    form_wf.store()
-
-    carddef.workflow = card_wf
-    carddef.store()
-
-    formdef.workflow = form_wf
-    formdef.store()
-
-    carddata = carddef.data_class()()
-    carddata.data = {'0': 'Foo'}
-    carddata.store()
-    carddata.just_created()
-    carddata.perform_workflow()
-
-    carddata_reloaded = carddata.get(carddata.id)
-    assert carddata_reloaded.data['0'] == 'HELLO'
-    assert carddata_reloaded.status == 'wf-2'
diff --git a/tests/workflow/test_carddata.py b/tests/workflow/test_carddata.py
new file mode 100644
index 00000000..692289e8
--- /dev/null
+++ b/tests/workflow/test_carddata.py
@@ -0,0 +1,855 @@
+import datetime
+import os
+
+import pytest
+from quixote import cleanup
+
+from wcs import sessions
+from wcs.carddef import CardDef
+from wcs.fields import DateField, FileField, ItemField, MapField, StringField
+from wcs.formdef import FormDef
+from wcs.qommon.http_request import HTTPRequest
+from wcs.qommon.upload_storage import PicklableUpload
+from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
+from wcs.wf.create_formdata import CreateFormdataWorkflowStatusItem, Mapping
+from wcs.wf.edit_carddata import EditCarddataWorkflowStatusItem
+from wcs.wf.jump import JumpWorkflowStatusItem
+from wcs.workflows import Workflow
+
+from ..utilities import clean_temporary_pub, create_temporary_pub
+
+
+def setup_module(module):
+    cleanup()
+
+
+def teardown_module(module):
+    clean_temporary_pub()
+
+
+def pytest_generate_tests(metafunc):
+    if 'two_pubs' in metafunc.fixturenames:
+        metafunc.parametrize('two_pubs', ['pickle', 'sql'], indirect=True)
+
+
+@pytest.fixture
+def pub(request):
+    pub = create_temporary_pub()
+    pub.cfg['language'] = {'language': 'en'}
+    pub.write_cfg()
+    req = HTTPRequest(None, {'SERVER_NAME': 'example.net', 'SCRIPT_NAME': ''})
+    req.response.filter = {}
+    req._user = None
+    pub._set_request(req)
+    req.session = sessions.BasicSession(id=1)
+    pub.set_config(req)
+    return pub
+
+
+@pytest.fixture
+def two_pubs(request):
+    pub = create_temporary_pub(sql_mode=(request.param == 'sql'))
+    pub.cfg['language'] = {'language': 'en'}
+    pub.write_cfg()
+    req = HTTPRequest(None, {'SERVER_NAME': 'example.net', 'SCRIPT_NAME': ''})
+    req.response.filter = {}
+    req._user = None
+    pub._set_request(req)
+    req.session = sessions.BasicSession(id=1)
+    pub.set_config(req)
+    return pub
+
+
+def test_create_carddata(two_pubs):
+    CardDef.wipe()
+    FormDef.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
+
+    carddef = CardDef()
+    carddef.name = 'My card'
+    carddef.fields = [
+        StringField(id='1', label='string'),
+        ItemField(id='2', label='List', items=['item1', 'item2'], varname='clist'),
+        DateField(id='3', label='Date', varname='cdate'),
+        FileField(id='4', label='File', varname='cfile'),
+    ]
+    carddef.store()
+
+    wf = Workflow(name='create-carddata')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    create = CreateCarddataWorkflowStatusItem()
+    create.label = 'Create CardDef'
+    create.varname = 'mycard'
+    create.id = '_create'
+    create.formdef_slug = carddef.url_name
+    create.mappings = [
+        Mapping(field_id='1', expression='=form_var_undefined'),
+        Mapping(field_id='2', expression='{{ form_var_list }}'),
+        Mapping(field_id='3', expression='{{ form_var_date }}'),
+        Mapping(field_id='4', expression='{{ form_var_file|default_if_none:"" }}'),
+    ]
+    create.parent = wf.possible_status[1]
+    wf.possible_status[1].items.insert(0, create)
+    wf.store()
+
+    formdef = FormDef()
+    formdef.name = 'source form'
+    formdef.fields = [
+        ItemField(id='1', label='List', items=['item1', 'item2'], varname='list'),
+        DateField(id='2', label='Date', varname='date'),
+        FileField(id='3', label='File', varname='file'),
+    ]
+    formdef.workflow_id = wf.id
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+
+    assert carddef.data_class().count() == 0
+
+    formdata = formdef.data_class()()
+    formdata.data = {}
+
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+
+    if two_pubs.is_using_postgresql():
+        errors = two_pubs.loggederror_class.select()
+        assert len(errors) == 2
+        assert any('form_var_undefined' in (error.exception_message or '') for error in errors)
+        assert any('invalid date value' in (error.exception_message or '') for error in errors)
+
+    formdata = formdef.data_class()()
+    today = datetime.date.today()
+
+    upload = PicklableUpload('test.jpeg', 'image/jpeg')
+    with open(os.path.join(os.path.dirname(__file__), '..', 'image-with-gps-data.jpeg'), 'rb') as jpg:
+        upload.receive([jpg.read()])
+
+    formdata.data = {'1': 'item1', '1_display': 'item1', '2': today.timetuple(), '3': upload}
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert formdata.get_substitution_variables()['form_links_mycard_form_number'] == '1-2'
+    carddata = carddef.data_class().get(id=2)
+    assert carddata.data['2'] == 'item1'
+    assert carddata.data['2_display'] == 'item1'
+    assert carddata.data['3'] == today.timetuple()
+    assert carddata.data['4'].base_filename == 'test.jpeg'
+
+    create.condition = {'type': 'python', 'value': '1 == 2'}
+    wf.store()
+    del formdef._workflow
+    carddef.data_class().wipe()
+    assert carddef.data_class().count() == 0
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 0
+
+
+def test_create_carddata_with_map_field(two_pubs):
+    CardDef.wipe()
+    FormDef.wipe()
+    if two_pubs.is_using_postgresql():
+        two_pubs.loggederror_class.wipe()
+
+    carddef = CardDef()
+    carddef.name = 'My card'
+    carddef.fields = [
+        MapField(id='1', label='map', varname='map'),
+    ]
+    carddef.store()
+
+    wf = Workflow(name='create-carddata')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    create = CreateCarddataWorkflowStatusItem()
+    create.label = 'Create CardDef'
+    create.varname = 'mycard'
+    create.id = '_create'
+    create.formdef_slug = carddef.url_name
+    create.mappings = [
+        Mapping(field_id='1', expression=''),
+    ]
+    create.parent = wf.possible_status[1]
+    wf.possible_status[1].items.insert(0, create)
+    wf.store()
+
+    formdef = FormDef()
+    formdef.name = 'source form'
+    formdef.fields = [
+        MapField(id='1', label='map', varname='map'),
+    ]
+    formdef.workflow_id = wf.id
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+    assert carddef.data_class().count() == 0
+
+    # empty value
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 1
+    assert not carddef.data_class().select()[0].data.get('1')
+
+    # valid coordinates
+    create.mappings[0].expression = '1;2'
+    wf.store()
+    formdef.refresh_from_storage()
+    carddef.data_class().wipe()
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].data.get('1') == '1;2'
+
+    # invalid value
+    create.mappings[0].expression = 'plop'
+    wf.store()
+    formdef.refresh_from_storage()
+    carddef.data_class().wipe()
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 1
+    assert not carddef.data_class().select()[0].data.get('1')
+
+    if two_pubs.is_using_postgresql():
+        errors = two_pubs.loggederror_class.select()
+        assert len(errors) == 1
+        assert any('invalid coordinates' in (error.exception_message or '') for error in errors)
+
+    # value from formdata
+    create.mappings[0].expression = '{{ form_var_map }}'
+    wf.store()
+    formdef.refresh_from_storage()
+    carddef.data_class().wipe()
+    formdata = formdef.data_class()()
+    formdata.data = {'1': '2;3'}
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].data.get('1') == '2;3'
+
+
+def test_create_carddata_user_association(two_pubs):
+    CardDef.wipe()
+    FormDef.wipe()
+    two_pubs.user_class.wipe()
+
+    user = two_pubs.user_class()
+    user.email = 'test@example.net'
+    user.name_identifiers = ['xyz']
+    user.store()
+
+    carddef = CardDef()
+    carddef.name = 'My card'
+    carddef.fields = []
+    carddef.user_support = 'optional'
+    carddef.store()
+    carddef.data_class().wipe()
+
+    wf = Workflow(name='create-carddata')
+    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
+    create = CreateCarddataWorkflowStatusItem()
+    create.label = 'Create CardDef'
+    create.varname = 'mycard'
+    create.id = '_create'
+    create.formdef_slug = carddef.url_name
+    create.map_fields_by_varname = True
+    create.parent = wf.possible_status[1]
+    wf.possible_status[1].items.insert(0, create)
+    wf.store()
+
+    formdef = FormDef()
+    formdef.name = 'source form'
+    formdef.fields = []
+    formdef.workflow_id = wf.id
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user is None
+
+    # keep user
+    carddef.data_class().wipe()
+    create.user_association_mode = 'keep-user'
+    wf.store()
+
+    formdata = FormDef.get(formdef.id).data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user.id == user.id
+
+    # user association on direct user
+    carddef.data_class().wipe()
+    create.user_association_mode = 'custom'
+    create.user_association_template = '{{ form_user }}'
+    wf.store()
+
+    formdata = FormDef.get(formdef.id).data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user.id == user.id
+
+    # user association on user email
+    carddef.data_class().wipe()
+    create.user_association_mode = 'custom'
+    create.user_association_template = 'test@example.net'
+    wf.store()
+
+    formdata = FormDef.get(formdef.id).data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user.id == user.id
+
+    # user association on name id
+    carddef.data_class().wipe()
+    create.user_association_mode = 'custom'
+    create.user_association_template = 'xyz'
+    wf.store()
+
+    formdata = FormDef.get(formdef.id).data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user.id == user.id
+
+    # user association on invalid user
+    carddef.data_class().wipe()
+    create.user_association_mode = 'custom'
+    create.user_association_template = 'zzz'
+    wf.store()
+
+    formdata = FormDef.get(formdef.id).data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    if two_pubs.loggederror_class:
+        two_pubs.loggederror_class.wipe()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user is None
+    if two_pubs.loggederror_class:
+        assert two_pubs.loggederror_class.count() == 1
+        logged_error = two_pubs.loggederror_class.select()[0]
+        assert logged_error.summary == 'Failed to attach user (not found: "zzz")'
+        assert logged_error.formdata_id == str(carddef.data_class().select()[0].id)
+
+    # user association on invalid template
+    carddef.data_class().wipe()
+    create.user_association_mode = 'custom'
+    create.user_association_template = '{% %}'
+    wf.store()
+
+    formdata = FormDef.get(formdef.id).data_class()()
+    formdata.data = {}
+    formdata.user_id = user.id
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    assert carddef.data_class().select()[0].user is None
+
+
+def test_edit_carddata_with_data_sourced_object(pub):
+    FormDef.wipe()
+    CardDef.wipe()
+
+    datasource = {
+        'type': 'formula',
+        'value': repr(
+            [
+                {'id': 'b', 'text': 'baker', 'extra': 'plop'},
+                {'id': 'c', 'text': 'cook', 'extra': 'plop2'},
+                {'id': 'l', 'text': 'lawyer', 'extra': 'plop3'},
+            ]
+        ),
+    }
+    carddef = CardDef()
+    carddef.name = 'Person'
+    carddef.fields = [
+        StringField(id='0', label='First Name', varname='first_name'),
+        StringField(id='1', label='Last Name', varname='last_name'),
+        ItemField(id='2', label='Profession', type='item', varname='profession', data_source=datasource),
+    ]
+    carddef.digest_templates = {'default': '{{ form_var_first_name }} {{ form_var_last_name }}'}
+    carddef.store()
+
+    carddata = carddef.data_class()()
+    carddata.data = {'0': 'Foo', '1': 'Bar', '2': 'l'}
+    carddata.data['2_display'] = carddef.fields[2].store_display_value(carddata.data, '2')
+    carddata.data['2_structured'] = carddef.fields[2].store_structured_value(carddata.data, '2')
+    carddata.store()
+
+    wf = Workflow(name='Card update')
+    st1 = wf.add_status('Update card', 'st1')
+
+    edit = EditCarddataWorkflowStatusItem()
+    edit.formdef_slug = carddef.url_name
+    edit.mappings = [
+        Mapping(field_id='2', expression='{{ form_var_new_profession }}'),
+    ]
+    edit.id = 'edit'
+    st1.items.append(edit)
+    edit.parent = st1
+    wf.store()
+
+    datasource = {'type': 'carddef:%s' % carddef.url_name}
+    formdef = FormDef()
+    formdef.name = 'Persons'
+    formdef.fields = [
+        ItemField(id='0', label='Person', type='item', varname='person', data_source=datasource),
+        StringField(id='1', label='New profession', varname='new_profession'),
+    ]
+    formdef.workflow = wf
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {'0': '1', '1': 'c'}
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    data = carddef.data_class().select()[0]
+    assert data.data['2'] == 'c'
+    assert data.data['2_display'] == 'cook'
+    assert data.data['2_structured'] == {'id': 'c', 'text': 'cook', 'extra': 'plop2'}
+
+    formdata = formdef.data_class()()
+    formdata.data = {'0': '1', '1': 'b'}
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    data = carddef.data_class().select()[0]
+    assert data.data['2'] == 'b'
+    assert data.data['2_display'] == 'baker'
+    assert data.data['2_structured'] == {'id': 'b', 'text': 'baker', 'extra': 'plop'}
+
+    # reset data
+    for expression in ('=None', '', '""'):
+        edit.mappings = [
+            Mapping(field_id='2', expression=expression),
+        ]
+        wf.store()
+
+        formdata = formdef.data_class()()
+        formdata.data = {'0': '1', '1': 'b'}
+        formdata.store()
+        formdata.just_created()
+        formdata.perform_workflow()
+
+        carddata = carddef.data_class().select()[0]
+        assert carddata.data['2'] in [None, '', '""']
+        assert carddata.data.get('2_display') is None
+        assert carddata.data.get('2_structured') is None
+
+        # restore initial data
+        carddata.data = data.data
+        carddata.store()
+
+
+def test_edit_carddata_with_linked_object(pub):
+    FormDef.wipe()
+    CardDef.wipe()
+
+    carddef = CardDef()
+    carddef.name = 'Parent'
+    carddef.fields = [
+        StringField(id='0', label='First Name', varname='first_name'),
+        StringField(id='1', label='Last Name', varname='last_name'),
+        StringField(id='2', label='Kids number', varname='kids_number'),
+    ]
+    carddef.store()
+
+    wf = Workflow(name='Card create and update')
+    st1 = wf.add_status('Create card', 'st1')
+    edit = CreateCarddataWorkflowStatusItem()
+    edit.formdef_slug = carddef.url_name
+    edit.mappings = [
+        Mapping(field_id='0', expression='{{ form_var_first_name }}'),
+        Mapping(field_id='1', expression='{{ form_var_last_name }}'),
+        Mapping(field_id='2', expression='{{ form_var_kids_number|default:"0" }}'),
+    ]
+    st1.items.append(edit)
+    edit.parent = st1
+    jump = JumpWorkflowStatusItem()
+    jump.id = '_jump'
+    jump.by = ['_submitter', '_receiver']
+    jump.status = 'st2'
+    st1.items.append(jump)
+    jump.parent = st1
+
+    st2 = wf.add_status('Update card', 'st2')
+    edit = EditCarddataWorkflowStatusItem()
+    edit.formdef_slug = carddef.url_name
+    edit.mappings = [
+        Mapping(field_id='2', expression='{{ form_var_kids_number|add:"1" }}'),
+    ]
+    edit.id = 'edit'
+    st2.items.append(edit)
+    edit.parent = st2
+    wf.store()
+
+    formdef = FormDef()
+    formdef.name = 'Parents'
+    formdef.fields = [
+        StringField(id='0', label='First Name', varname='first_name'),
+        StringField(id='1', label='Last Name', varname='last_name'),
+        StringField(id='2', label='Number of kids', varname='kids_number'),
+    ]
+    formdef.workflow = wf
+    formdef.store()
+
+    formdata = formdef.data_class()()
+    formdata.data = {'0': 'Parent', '1': 'Foo', '2': '2'}
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 1
+    card_data = carddef.data_class().select()[0]
+    assert card_data.data['2'] == '3'
+
+
+def test_edit_carddata_manual_targeting(two_pubs):
+    if not two_pubs.is_using_postgresql():
+        pytest.skip('this requires SQL')
+        return
+
+    FormDef.wipe()
+    CardDef.wipe()
+    two_pubs.loggederror_class.wipe()
+
+    # carddef
+    carddef = CardDef()
+    carddef.name = 'Parent'
+    carddef.fields = [
+        StringField(id='0', label='First Name', varname='first_name'),
+        StringField(id='1', label='Last Name', varname='last_name'),
+        StringField(id='2', label='Kids number', varname='kids_number'),
+    ]
+    carddef.store()
+    carddef.data_class().wipe()
+
+    # and sample carddatas
+    for i in range(1, 4):
+        carddata = carddef.data_class()()
+        carddata.data = {
+            '0': 'First name %s' % i,
+            '1': 'Last name %s' % i,
+            '2': '0',
+        }
+        carddata.store()
+
+    # formdef workflow that will update carddata
+    wf = Workflow(name='Card create and update')
+    st1 = wf.add_status('Create card', 'st1')
+    # create linked carddata
+    edit = CreateCarddataWorkflowStatusItem()
+    edit.formdef_slug = carddef.url_name
+    edit.mappings = [
+        Mapping(field_id='0', expression='{{ form_var_first_name }}'),
+        Mapping(field_id='1', expression='{{ form_var_last_name }}'),
+        Mapping(field_id='2', expression='{{ form_var_kids_number|default:"0" }}'),
+    ]
+    st1.items.append(edit)
+    edit.parent = st1
+    jump = JumpWorkflowStatusItem()
+    jump.id = '_jump'
+    jump.by = ['_submitter', '_receiver']
+    jump.status = 'st2'
+    st1.items.append(jump)
+    jump.parent = st1
+
+    st2 = wf.add_status('Update card', 'st2')
+    edit = EditCarddataWorkflowStatusItem()
+    edit.formdef_slug = carddef.url_name
+    edit.target_mode = 'manual'  # not configured
+    edit.mappings = [
+        Mapping(field_id='2', expression='{{ form_var_kids_number|add:"1" }}'),
+    ]
+    edit.id = 'edit'
+    st2.items.append(edit)
+    edit.parent = st2
+    wf.store()
+
+    # associated formdef
+    formdef = FormDef()
+    formdef.name = 'Parents'
+    datasource = {'type': 'carddef:%s' % carddef.url_name}
+    formdef.fields = [
+        StringField(id='0', label='First Name', varname='first_name'),
+        StringField(id='1', label='Last Name', varname='last_name'),
+        StringField(id='2', label='Number of kids', varname='kids_number'),
+        ItemField(id='3', label='Card', type='item', varname='card', data_source=datasource),
+        StringField(id='4', label='string', varname='string'),
+    ]
+    formdef.workflow = wf
+    formdef.store()
+
+    # create formdatas
+
+    # target not configured
+    formdata = formdef.data_class()()
+    formdata.data = {
+        '0': 'Parent',
+        '1': 'Foo',
+        '2': '2',
+        '3': '3',  # set from datasource
+        '4': '1',
+    }
+    # set parent
+    formdata.submission_context = {
+        'orig_object_type': 'carddef',
+        'orig_formdata_id': '2',
+        'orig_formdef_id': str(carddef.id),
+    }
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+
+    assert carddef.data_class().count() == 4
+    assert carddef.data_class().get(1).data['2'] == '0'
+    assert carddef.data_class().get(2).data['2'] == '0'
+    assert carddef.data_class().get(3).data['2'] == '0'
+    assert carddef.data_class().get(4).data['2'] == '2'
+    assert two_pubs.loggederror_class.count() == 0
+
+    # configure target
+    edit.target_id = '{{ form_var_string }}'  # == '1'
+    wf.store()
+    formdata = formdef.data_class()()
+    formdata.data = {
+        '0': 'Parent',
+        '1': 'Foo',
+        '2': '2',
+        '3': '3',  # set from datasource
+        '4': '1',
+    }
+    # set parent
+    formdata.submission_context = {
+        'orig_object_type': 'carddef',
+        'orig_formdata_id': '2',
+        'orig_formdef_id': str(carddef.id),
+    }
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 5
+    assert carddef.data_class().get(1).data['2'] == '3'  # 2 + 1
+    assert carddef.data_class().get(2).data['2'] == '0'
+    assert carddef.data_class().get(3).data['2'] == '0'
+    assert carddef.data_class().get(4).data['2'] == '2'
+    assert carddef.data_class().get(5).data['2'] == '2'
+    assert two_pubs.loggederror_class.count() == 0
+
+    # target not found
+    edit.target_id = '42{{ form_var_string }}'  # == '421'
+    wf.store()
+    formdata = formdef.data_class()()
+    formdata.data = {
+        '0': 'Parent',
+        '1': 'Foo',
+        '2': '2',
+        '3': '3',  # set from datasource
+        '4': '1',
+    }
+    # set parent
+    formdata.submission_context = {
+        'orig_object_type': 'carddef',
+        'orig_formdata_id': '2',
+        'orig_formdef_id': str(carddef.id),
+    }
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 6
+    assert carddef.data_class().get(1).data['2'] == '3'  # not changed
+    assert carddef.data_class().get(2).data['2'] == '0'
+    assert carddef.data_class().get(3).data['2'] == '0'
+    assert carddef.data_class().get(4).data['2'] == '2'
+    assert carddef.data_class().get(5).data['2'] == '2'
+    assert carddef.data_class().get(6).data['2'] == '2'
+    assert two_pubs.loggederror_class.count() == 1
+    logged_error = two_pubs.loggederror_class.select()[0]
+    assert logged_error.summary == 'Could not find targeted "Parent" object by id 421'
+
+    # slug not or badly configured
+    edit.target_id = '{{ form_var_string }}'  # == '1'
+    edit.formdef_slug = None
+    wf.store()
+    formdata = formdef.data_class()()
+    formdata.data = {
+        '0': 'Parent',
+        '1': 'Foo',
+        '2': '3',
+        '3': '3',  # set from datasource
+        '4': '1',
+    }
+    # set parent
+    formdata.submission_context = {
+        'orig_object_type': 'carddef',
+        'orig_formdata_id': '2',
+        'orig_formdef_id': str(carddef.id),
+    }
+    formdata.store()
+    formdata.just_created()
+    formdata.perform_workflow()
+    assert carddef.data_class().count() == 7
+    assert carddef.data_class().get(1).data['2'] == '3'  # not changed
+    assert carddef.data_class().get(2).data['2'] == '0'
+    assert carddef.data_class().get(3).data['2'] == '0'
+    assert carddef.data_class().get(4).data['2'] == '2'
+    assert carddef.data_class().get(5).data['2'] == '2'
+    assert carddef.data_class().get(6).data['2'] == '2'
+    assert carddef.data_class().get(7).data['2'] == '3'
+
+
+def test_edit_carddata_targeting_itself(pub):
+    CardDef.wipe()
+    carddef = CardDef()
+    carddef.name = 'Foo Card'
+    carddef.fields = [
+        StringField(id='0', label='foo', varname='foo'),
+    ]
+    carddef.store()
+    carddef.data_class().wipe()
+
+    # card workflow: update itself then jump to second status
+    card_wf = Workflow(name='Card workflow')
+    st1 = card_wf.add_status('Status1')
+    st2 = card_wf.add_status('Status2')
+
+    edit = EditCarddataWorkflowStatusItem()
+    edit.id = '_edit'
+    edit.formdef_slug = carddef.url_name
+    edit.target_mode = 'manual'
+    edit.target_id = '{{ form_internal_id }}'  # itself
+    edit.mappings = [
+        Mapping(field_id='0', expression='bar {{ form_internal_id }}'),
+    ]
+    edit.parent = st1
+    st1.items.append(edit)
+
+    jump = JumpWorkflowStatusItem()
+    jump.id = '_jump'
+    jump.status = st2.id
+    st1.items.append(jump)
+    jump.parent = st1
+
+    card_wf.store()
+
+    carddef.workflow = card_wf
+    carddef.store()
+
+    # create some cardata
+    for i in range(1, 4):
+        carddata = carddef.data_class()()
+        carddata.data = {
+            '0': 'foo %s' % i,
+        }
+        carddata.store()
+        # run workflow, verify that carddata is modified
+        carddata.just_created()
+        carddata.perform_workflow()
+        assert carddata.data['0'] == 'bar %s' % carddata.id
+        assert carddata.status == 'wf-%s' % st2.id
+
+
+def test_edit_carddata_from_created_object(pub):
+    FormDef.wipe()
+    CardDef.wipe()
+
+    carddef = CardDef()
+    carddef.name = 'Card'
+    carddef.fields = [
+        StringField(id='0', label='Card Field', varname='card_field'),
+    ]
+    carddef.store()
+
+    formdef = FormDef()
+    formdef.name = 'Form'
+    formdef.fields = [
+        StringField(id='0', label='Form Field', varname='form_field'),
+    ]
+    formdef.store()
+
+    # card workflow: create formdata then jump to second status
+    card_wf = Workflow(name='Card workflow')
+    st1 = card_wf.add_status('Status1')
+    st2 = card_wf.add_status('Status2')
+
+    create = CreateFormdataWorkflowStatusItem()
+    create.id = '_create'
+    create.formdef_slug = formdef.url_name
+    create.mappings = [
+        Mapping(field_id='0', expression='...'),
+    ]
+    create.parent = st1
+    st1.items.append(create)
+
+    jump = JumpWorkflowStatusItem()
+    jump.id = '_jump'
+    jump.status = st2.id
+    st1.items.append(jump)
+    jump.parent = st1
+
+    # form workflow: edit parent card data
+    form_wf = Workflow(name='Form workflow')
+    st1 = form_wf.add_status('Status1')
+    edit = EditCarddataWorkflowStatusItem()
+    edit.formdef_slug = carddef.url_name
+    edit.mappings = [
+        Mapping(field_id='0', expression='HELLO'),
+    ]
+    edit.id = 'edit'
+    st1.items.append(edit)
+    edit.parent = st1
+    form_wf.store()
+
+    carddef.workflow = card_wf
+    carddef.store()
+
+    formdef.workflow = form_wf
+    formdef.store()
+
+    carddata = carddef.data_class()()
+    carddata.data = {'0': 'Foo'}
+    carddata.store()
+    carddata.just_created()
+    carddata.perform_workflow()
+
+    carddata_reloaded = carddata.get(carddata.id)
+    assert carddata_reloaded.data['0'] == 'HELLO'
+    assert carddata_reloaded.status == 'wf-2'
-- 
2.30.2

