Projet

Général

Profil

0002-formdata-store-data-history-62800.patch

Lauréline Guérin, 07 octobre 2022 11:37

Télécharger (45,6 ko)

Voir les différences:

Subject: [PATCH 2/3] formdata: store data history (#62800)

 tests/form_pages/test_all.py      |   2 +-
 tests/form_pages/test_formdata.py |  24 +-
 tests/test_content_snapshots.py   | 672 ++++++++++++++++++++++++++++++
 tests/workflow/test_all.py        |  53 ++-
 wcs/api.py                        |   6 +
 wcs/formdata.py                   |   9 +-
 wcs/forms/root.py                 |   5 +-
 wcs/wf/backoffice_fields.py       |   9 +-
 wcs/wf/edit_carddata.py           |  12 +-
 wcs/workflows.py                  |   6 +
 10 files changed, 759 insertions(+), 39 deletions(-)
 create mode 100644 tests/test_content_snapshots.py
tests/form_pages/test_all.py
9123 9123
        '1_display': 'un',
9124 9124
        '1_structured': {'id': '1', 'text': 'un', 'more': 'foo'},
9125 9125
    }
9126
    assert '2020-04-18' in formdata.evolution[0].parts[0].content
9126
    assert '2020-04-18' in formdata.evolution[0].parts[1].content
9127 9127

  
9128 9128

  
9129 9129
def test_exclude_self_condition(pub):
tests/form_pages/test_formdata.py
21 21
from wcs.wf.create_formdata import Mapping
22 22
from wcs.wf.export_to_model import transform_to_pdf
23 23
from wcs.wf.form import WorkflowFormFieldsFormDef
24
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef
24
from wcs.workflows import ContentSnapshotPart, Workflow, WorkflowBackofficeFieldsFormDef
25 25
from wcs.wscalls import NamedWsCall
26 26

  
27 27
from ..utilities import clean_temporary_pub, create_temporary_pub, get_app, login
......
292 292
    assert bo1.get_content() == b'foobar'
293 293

  
294 294
    # but nothing in history
295
    for evo in formdata.evolution:
296
        assert not evo.parts
295
    assert len(formdata.evolution) == 2
296
    assert len(formdata.evolution[0].parts) == 1
297
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
298
    assert formdata.evolution[1].parts is None
297 299

  
298 300

  
299 301
def test_formdata_attachment_file_options(pub):
......
1451 1453
    assert 'The form has been recorded and: XbarY' in resp.text
1452 1454

  
1453 1455
    formdata = formdef.data_class().select()[0]
1454
    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
1456
    assert formdata.evolution[0].parts[1].content == 'Hello bar World'
1455 1457

  
1456 1458
    # check with publisher variable in named webservice call
1457 1459
    if not pub.site_options.has_section('variables'):
......
1476 1478
    assert 'The form has been recorded and: XbarY' in resp.text
1477 1479

  
1478 1480
    formdata = formdef.data_class().select()[0]
1479
    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
1481
    assert formdata.evolution[0].parts[1].content == 'Hello bar World'
1480 1482

  
1481 1483

  
1482 1484
def test_formdata_named_wscall_in_conditions(http_requests, pub):
......
1602 1604
    assert 'The form has been recorded' in resp.text
1603 1605

  
1604 1606
    formdata = formdef.data_class().select()[0]
1605
    assert formdata.evolution[0].parts[0].content == 'Hello World'
1606
    assert formdata.evolution[0].parts[0].to is None
1607
    assert formdata.evolution[0].parts[1].content == 'Hello World'
1608
    assert formdata.evolution[0].parts[1].to is None
1607 1609
    resp = app.get('/test/%s/' % formdata.id)
1608 1610
    resp.status_int = 200
1609 1611
    assert resp.html.find('div', {'id': 'evolution-log'}).find('p').text == 'Hello World'
......
1622 1624
    assert 'The form has been recorded' in resp.text
1623 1625

  
1624 1626
    formdata = formdef.data_class().select()[0]
1625
    assert formdata.evolution[0].parts[0].content == 'Hello World'
1626
    assert formdata.evolution[0].parts[0].to == [role1.id]
1627
    assert formdata.evolution[0].parts[1].content == 'Hello World'
1628
    assert formdata.evolution[0].parts[1].to == [role1.id]
1627 1629
    resp = app.get('/test/%s/' % formdata.id)
1628 1630
    resp.status_int = 200
1629 1631
    assert not resp.html.find('div', {'id': 'evolution-log'}).find('p')
......
1642 1644
    assert 'The form has been recorded' in resp.text
1643 1645

  
1644 1646
    formdata = formdef.data_class().select()[0]
1645
    assert formdata.evolution[0].parts[0].content == 'Hello World'
1646
    assert formdata.evolution[0].parts[0].to == [role2.id]
1647
    assert formdata.evolution[0].parts[1].content == 'Hello World'
1648
    assert formdata.evolution[0].parts[1].to == [role2.id]
1647 1649
    resp = app.get('/test/%s/' % formdata.id)
1648 1650
    resp.status_int = 200
1649 1651
    assert resp.html.find('div', {'id': 'evolution-log'}).find('p').text == 'Hello World'
tests/test_content_snapshots.py
1
import json
2
import os
3

  
4
import pytest
5
from webtest import Upload
6

  
7
from wcs import fields
8
from wcs.api_access import ApiAccess
9
from wcs.carddef import CardDef
10
from wcs.formdef import FormDef
11
from wcs.qommon.http_request import HTTPRequest
12
from wcs.qommon.ident.password_accounts import PasswordAccount
13
from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem
14
from wcs.wf.create_formdata import Mapping
15
from wcs.workflows import ContentSnapshotPart, Workflow, WorkflowBackofficeFieldsFormDef
16

  
17
from .utilities import clean_temporary_pub, create_temporary_pub, get_app, login
18

  
19

  
20
@pytest.fixture
21
def pub(emails):
22
    pub = create_temporary_pub()
23

  
24
    req = HTTPRequest(None, {'SCRIPT_NAME': '/', 'SERVER_NAME': 'example.net'})
25
    pub.set_app_dir(req)
26
    pub.cfg['identification'] = {'methods': ['password']}
27
    pub.cfg['language'] = {'language': 'en'}
28
    pub.write_cfg()
29
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
30
        fd.write(
31
            '''
32
    [api-secrets]
33
    coucou = 1234
34
    '''
35
        )
36

  
37
    return pub
38

  
39

  
40
def teardown_module(module):
41
    clean_temporary_pub()
42

  
43

  
44
@pytest.fixture
45
def role(pub):
46
    pub.role_class.wipe()
47
    role = pub.role_class(name='foobar')
48
    role.store()
49
    return role
50

  
51

  
52
@pytest.fixture
53
def user(pub, role):
54
    pub.user_class.wipe()
55
    user = pub.user_class()
56
    user.name = 'Jean Darmette'
57
    user.email = 'jean.darmette@triffouilis.fr'
58
    user.name_identifiers = ['0123456789']
59
    user.roles = [role.id]
60
    user.store()
61

  
62
    account = PasswordAccount(id='admin')
63
    account.set_password('admin')
64
    account.user_id = user.id
65
    account.store()
66

  
67
    return user
68

  
69

  
70
@pytest.fixture
71
def access(pub, role):
72
    ApiAccess.wipe()
73
    access = ApiAccess()
74
    access.name = 'test'
75
    access.access_identifier = 'test'
76
    access.access_key = '12345'
77
    access.roles = [role]
78
    access.store()
79
    return access
80

  
81

  
82
def test_formdata_create_and_edit_and_bo_field(pub, user):
83
    FormDef.wipe()
84
    formdef = FormDef()
85
    formdef.name = 'test'
86
    formdef.fields = [
87
        fields.StringField(id='1', label='string', varname='foo'),
88
    ]
89
    formdef.store()
90
    formdef.data_class().wipe()
91

  
92
    Workflow.wipe()
93
    workflow = Workflow(name='test')
94
    workflow.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(workflow)
95
    workflow.backoffice_fields_formdef.fields = [
96
        fields.StringField(id='bo1', label='bo field 1', type='string', varname='plop'),
97
    ]
98
    st1 = workflow.add_status('Status1', 'st1')
99
    setbo = st1.add_action('set-backoffice-fields')
100
    setbo.fields = [{'field_id': 'bo1', 'value': '{{ form_var_foo }}'}]
101
    setbo2 = st1.add_action('set-backoffice-fields')
102
    setbo2.fields = [{'field_id': 'bo1', 'value': '{{ "foo"|add:form_var_plop }}'}]
103
    jump = st1.add_action('jump')
104
    jump.status = 'st2'
105

  
106
    st2 = workflow.add_status('Status2', 'st2')
107

  
108
    editable = st2.add_action('editable', id='_editable')
109
    editable.by = ['_submitter']
110
    editable.status = st1.id
111
    workflow.store()
112

  
113
    formdef.workflow_id = workflow.id
114
    formdef.store()
115
    formdef.data_class().wipe()
116

  
117
    app = login(get_app(pub))
118
    resp = app.get('/test/')
119
    resp.form['f1'] = 'bar'
120
    resp = resp.form.submit('submit')  # -> validation
121
    resp = resp.form.submit('submit').follow()  # -> submitted
122
    assert 'The form has been recorded' in resp.text
123

  
124
    data_id = formdef.data_class().select()[0].id
125
    resp = app.get('/test/%s/' % data_id)
126
    assert 'button_editable-button' in resp.text
127

  
128
    resp = resp.form.submit('button_editable')
129
    resp = resp.follow()
130
    assert resp.form['f1'].value == 'bar'
131
    resp.form['f1'].value = 'baz'
132
    resp = resp.form.submit('submit').follow()  # -> saved
133

  
134
    formdata = formdef.data_class().get(data_id)
135
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
136
    # creation, submit
137
    assert formdata.evolution[0].parts[0].old_data == {}
138
    assert formdata.evolution[0].parts[0].new_data == {'1': 'bar', 'bo1': None}
139
    # creation, bo field first action
140
    assert formdata.evolution[0].parts[1].old_data == {'1': 'bar'}
141
    assert formdata.evolution[0].parts[1].new_data == {'1': 'bar', 'bo1': 'bar'}
142
    # creation, bo field second action
143
    assert formdata.evolution[0].parts[2].old_data == {'1': 'bar', 'bo1': 'bar'}
144
    assert formdata.evolution[0].parts[2].new_data == {'1': 'bar', 'bo1': 'foobar'}
145
    # update, submit
146
    assert formdata.evolution[1].parts[0].old_data == {'1': 'bar', 'bo1': 'foobar'}
147
    assert formdata.evolution[1].parts[0].new_data == {'1': 'baz', 'bo1': 'foobar'}
148
    # update, bo field first action
149
    assert formdata.evolution[2].parts[0].old_data == {'1': 'baz', 'bo1': 'foobar'}
150
    assert formdata.evolution[2].parts[0].new_data == {'1': 'baz', 'bo1': 'baz'}
151
    # update, bo field second action
152
    assert formdata.evolution[2].parts[1].old_data == {'1': 'baz', 'bo1': 'baz'}
153
    assert formdata.evolution[2].parts[1].new_data == {'1': 'baz', 'bo1': 'foobaz'}
154

  
155

  
156
def test_backoffice_formdata_submission(pub, user):
157
    FormDef.wipe()
158
    formdef = FormDef()
159
    formdef.name = 'form title'
160
    formdef.fields = [
161
        fields.StringField(id='1', label='String', type='string'),
162
    ]
163
    formdef.workflow_roles = {'_receiver': user.roles[0]}
164
    formdef.backoffice_submission_roles = user.roles[:]
165
    formdef.store()
166

  
167
    app = login(get_app(pub))
168
    resp = app.get('/backoffice/submission/form-title/')
169
    resp.form['f1'] = 'test submission'
170
    resp = resp.form.submit('submit')  # -> validation
171
    resp = resp.form.submit('submit').follow()  # -> submitted
172
    assert 'The form has been recorded' in resp.text
173

  
174
    formdata = formdef.data_class().select()[0]
175
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
176
    assert formdata.evolution[0].parts[0].old_data == {}
177
    assert formdata.evolution[0].parts[0].new_data == {'1': 'test submission'}
178

  
179

  
180
def test_backoffice_carddata_add_edit(pub, user):
181
    CardDef.wipe()
182
    carddef = CardDef()
183
    carddef.name = 'test'
184
    carddef.fields = [
185
        fields.StringField(id='1', label='string', varname='foo'),
186
    ]
187
    carddef.backoffice_submission_roles = user.roles
188
    carddef.workflow_roles = {
189
        '_viewer': user.roles[0],
190
        '_editor': user.roles[0],
191
    }
192
    carddef.store()
193
    carddef.data_class().wipe()
194

  
195
    Workflow.wipe()
196
    workflow = Workflow(name='test')
197
    workflow.roles = {
198
        '_viewer': 'Viewer',
199
        '_editor': 'Editor',
200
    }
201
    st1 = workflow.add_status('Status1', 'st1')
202
    jump = st1.add_action('jump')
203
    jump.status = 'st2'
204
    st2 = workflow.add_status('Status2', 'st2')
205
    editable = st2.add_action('editable', id='_editable')
206
    editable.by = ['_editor']
207
    editable.status = st1.id
208
    workflow.store()
209

  
210
    carddef.workflow_id = workflow.id
211
    carddef.store()
212
    carddef.data_class().wipe()
213

  
214
    app = login(get_app(pub))
215
    resp = app.get('/backoffice/data/test/add/')
216
    resp.form['f1'] = 'foo'
217
    resp = resp.form.submit('submit').follow()
218
    assert 'button_editable-button' in resp.text
219

  
220
    resp = resp.form.submit('button_editable')
221
    resp = resp.follow()
222
    resp.form['f1'].value = 'bar'
223
    resp = resp.form.submit('submit').follow()
224

  
225
    carddata = carddef.data_class().select()[0]
226
    # creation
227
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
228
    assert carddata.evolution[0].parts[0].old_data == {}
229
    assert carddata.evolution[0].parts[0].new_data == {'1': 'foo'}
230
    # update
231
    assert isinstance(carddata.evolution[1].parts[0], ContentSnapshotPart)
232
    assert carddata.evolution[1].parts[0].old_data == {'1': 'foo'}
233
    assert carddata.evolution[1].parts[0].new_data == {'1': 'bar'}
234

  
235

  
236
def test_backoffice_card_import_csv(pub, user):
237
    CardDef.wipe()
238
    carddef = CardDef()
239
    carddef.name = 'test'
240
    carddef.fields = [
241
        fields.StringField(id='1', label='Test'),
242
        fields.StringField(id='2', label='Test2'),
243
    ]
244
    carddef.workflow_roles = {'_editor': user.roles[0]}
245
    carddef.backoffice_submission_roles = user.roles
246
    carddef.store()
247
    carddef.data_class().wipe()
248

  
249
    app = login(get_app(pub))
250

  
251
    resp = app.get(carddef.get_url())
252
    resp = resp.click('Import data from a file')
253
    data = [b'Test,Test2']
254
    data.append(b'data,foo')
255

  
256
    resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), 'text/csv')
257
    resp = resp.forms[0].submit().follow()
258
    assert 'Importing data into cards' in resp
259
    assert carddef.data_class().count() == 1
260
    carddata = carddef.data_class().select()[0]
261
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
262
    assert carddata.evolution[-1].parts[0].old_data == {}
263
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'data', '2': 'foo'}
264

  
265

  
266
def test_backoffice_card_import_json(pub, user):
267
    CardDef.wipe()
268
    carddef = CardDef()
269
    carddef.name = 'test'
270
    carddef.fields = [
271
        fields.StringField(id='1', label='Test', varname='string'),
272
    ]
273
    carddef.workflow_roles = {'_editor': user.roles[0]}
274
    carddef.backoffice_submission_roles = user.roles
275
    carddef.store()
276
    carddef.data_class().wipe()
277

  
278
    app = login(get_app(pub))
279
    resp = app.get(carddef.get_url())
280
    resp = resp.click('Import data from a file')
281
    data = {
282
        'data': [
283
            {
284
                'fields': {
285
                    'string': 'a string',
286
                }
287
            }
288
        ]
289
    }
290
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
291
    resp = resp.forms[0].submit()
292
    assert '/backoffice/processing?job=' in resp.location
293
    resp = resp.follow()
294

  
295
    carddata = carddef.data_class().select()[0]
296
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
297
    assert carddata.evolution[-1].parts[0].old_data == {}
298
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'a string'}
299

  
300

  
301
def test_workflow_formdata_create(pub):
302
    FormDef.wipe()
303
    pub.tracking_code_class.wipe()
304

  
305
    target_formdef = FormDef()
306
    target_formdef.name = 'target form'
307
    target_formdef.fields = [
308
        fields.StringField(id='0', label='string', varname='string'),
309
    ]
310
    target_formdef.store()
311

  
312
    wf = Workflow(name='create-formdata')
313
    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
314
    create = wf.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
315
    create.formdef_slug = target_formdef.url_name
316
    create.label = 'create a new linked form'
317
    create.varname = 'resubmitted'
318
    create.mappings = [
319
        Mapping(field_id='0', expression='{{ form_var_foo }}'),
320
    ]
321
    wf.store()
322

  
323
    source_formdef = FormDef()
324
    source_formdef.name = 'source form'
325
    source_formdef.fields = [
326
        fields.StringField(id='0', label='string', varname='foo'),
327
    ]
328
    source_formdef.workflow_id = wf.id
329
    source_formdef.store()
330

  
331
    formdata = source_formdef.data_class()()
332
    formdata.data = {'0': 'foobar'}
333
    formdata.just_created()
334
    formdata.store()
335

  
336
    formdata.perform_workflow()
337
    assert target_formdef.data_class().count() == 1
338
    created_formdata = target_formdef.data_class().select()[0]
339
    assert isinstance(created_formdata.evolution[0].parts[0], ContentSnapshotPart)
340
    assert created_formdata.evolution[0].parts[0].old_data == {}
341
    assert created_formdata.evolution[0].parts[0].new_data == {'0': 'foobar'}
342

  
343

  
344
def test_workflow_carddata_create(pub):
345
    CardDef.wipe()
346
    FormDef.wipe()
347

  
348
    carddef = CardDef()
349
    carddef.name = 'My card'
350
    carddef.fields = [
351
        fields.StringField(id='1', label='string'),
352
    ]
353
    carddef.store()
354

  
355
    wf = Workflow(name='create-carddata')
356
    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
357
    create = wf.possible_status[1].add_action('create_carddata', id='_create', prepend=True)
358
    create.label = 'Create CardDef'
359
    create.varname = 'mycard'
360
    create.formdef_slug = carddef.url_name
361
    create.mappings = [
362
        Mapping(field_id='1', expression='{{ form_var_string }}'),
363
    ]
364
    wf.store()
365

  
366
    formdef = FormDef()
367
    formdef.name = 'source form'
368
    formdef.fields = [
369
        fields.StringField(id='1', label='string', varname='string'),
370
    ]
371
    formdef.workflow_id = wf.id
372
    formdef.store()
373

  
374
    formdata = formdef.data_class()()
375
    formdata.data = {'1': 'foobar'}
376
    formdata.just_created()
377
    formdata.store()
378

  
379
    formdata.perform_workflow()
380
    assert carddef.data_class().count() == 1
381
    carddata = carddef.data_class().select()[0]
382
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
383
    assert carddata.evolution[-1].parts[0].old_data == {}
384
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'foobar'}
385

  
386

  
387
def test_workflow_carddata_edit(pub):
388
    FormDef.wipe()
389
    CardDef.wipe()
390

  
391
    # carddef
392
    carddef = CardDef()
393
    carddef.name = 'Model 1'
394
    carddef.fields = [
395
        fields.StringField(id='1', label='string', varname='string'),
396
    ]
397
    carddef.store()
398
    carddef.data_class().wipe()
399

  
400
    carddata = carddef.data_class()()
401
    carddata.data = {
402
        '1': 'foobar',
403
    }
404
    carddata.just_created()
405
    carddata.store()
406

  
407
    # formdef workflow that will update carddata
408
    wf = Workflow(name='update')
409
    st1 = wf.add_status('New', 'st1')
410
    jump = st1.add_action('jump', id='_jump')
411
    jump.by = ['_submitter', '_receiver']
412
    jump.status = 'st2'
413
    st2 = wf.add_status('Update card', 'st2')
414
    edit = st2.add_action('edit_carddata', id='edit')
415
    edit.formdef_slug = carddef.url_name
416
    edit.target_mode = 'manual'
417
    edit.target_id = '{{ form_var_card }}'
418
    edit.mappings = [
419
        Mapping(field_id='1', expression='{{ form_var_foo }}'),
420
    ]
421
    wf.store()
422

  
423
    # associated formdef
424
    formdef = FormDef()
425
    formdef.name = 'Update'
426
    formdef.fields = [
427
        fields.StringField(id='1', label='string', varname='card'),
428
        fields.StringField(id='2', label='string', varname='foo'),
429
    ]
430
    formdef.workflow = wf
431
    formdef.store()
432

  
433
    formdata = formdef.data_class()()
434
    formdata.data = {
435
        '1': str(carddata.id),
436
        '2': 'foobaz',
437
    }
438
    formdata.just_created()
439
    formdata.store()
440
    formdata.perform_workflow()
441
    carddata.refresh_from_storage()
442
    # creation
443
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
444
    assert carddata.evolution[0].parts[0].old_data == {}
445
    assert carddata.evolution[0].parts[0].new_data == {'1': 'foobar'}
446
    # update
447
    assert isinstance(carddata.evolution[1].parts[0], ContentSnapshotPart)
448
    assert carddata.evolution[1].parts[0].old_data == {'1': 'foobar'}
449
    assert carddata.evolution[1].parts[0].new_data == {'1': 'foobaz'}
450

  
451

  
452
def test_workflow_set_backoffice_field(http_requests, pub):
453
    Workflow.wipe()
454
    FormDef.wipe()
455
    wf = Workflow(name='xxx')
456
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
457
    wf.backoffice_fields_formdef.fields = [
458
        fields.StringField(id='bo1', label='1st backoffice field', type='string', varname='backoffice_blah'),
459
    ]
460
    st1 = wf.add_status('Status1')
461
    wf.store()
462

  
463
    formdef = FormDef()
464
    formdef.name = 'baz'
465
    formdef.fields = [
466
        fields.StringField(id='0', label='String', type='string', varname='string'),
467
    ]
468
    formdef.workflow_id = wf.id
469
    formdef.store()
470

  
471
    formdata = formdef.data_class()()
472
    formdata.data = {'0': 'HELLO'}
473
    formdata.just_created()
474
    formdata.store()
475
    pub.substitutions.feed(formdata)
476

  
477
    item = SetBackofficeFieldsWorkflowStatusItem()
478
    item.fields = [{'field_id': 'bo1', 'value': '{{ form_var_string }}'}]
479
    item.parent = st1
480

  
481
    item.perform(formdata)
482
    formdata.refresh_from_storage()
483
    # creation
484
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
485
    assert formdata.evolution[-1].parts[0].old_data == {}
486
    assert formdata.evolution[-1].parts[0].new_data == {
487
        '0': 'HELLO',
488
    }
489
    # bo action
490
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
491
    assert formdata.evolution[-1].parts[1].old_data == {
492
        '0': 'HELLO',
493
    }
494
    assert formdata.evolution[-1].parts[1].new_data == {
495
        '0': 'HELLO',
496
        'bo1': 'HELLO',
497
    }
498

  
499

  
500
def test_api_form_submit(pub, user, access, role):
501
    app = get_app(pub)
502
    app.set_authorization(('Basic', ('test', '12345')))
503

  
504
    FormDef.wipe()
505
    formdef = FormDef()
506
    formdef.name = 'test'
507
    formdef.fields = [
508
        fields.StringField(id='1', label='foobar', varname='foobar'),
509
    ]
510
    formdef.backoffice_submission_roles = [role.id]
511
    formdef.store()
512
    data_class = formdef.data_class()
513

  
514
    payload = {
515
        'data': {
516
            'foobar': 'xxx',
517
        }
518
    }
519
    resp = app.post_json(
520
        '/api/formdefs/test/submit',
521
        payload,
522
    )
523
    assert resp.json['err'] == 0
524
    formdata = data_class.get(resp.json['data']['id'])
525
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
526
    assert formdata.evolution[0].parts[0].old_data == {}
527
    assert formdata.evolution[0].parts[0].new_data == {'1': 'xxx'}
528

  
529

  
530
def test_api_formdata_edit(pub, user, access, role):
531
    app = get_app(pub)
532
    app.set_authorization(('Basic', ('test', '12345')))
533

  
534
    FormDef.wipe()
535
    formdef = FormDef()
536
    formdef.name = 'test'
537
    formdef.fields = [
538
        fields.StringField(id='0', label='foobar', varname='foobar'),
539
    ]
540
    Workflow.wipe()
541
    workflow = Workflow(name='foo')
542
    workflow.possible_status = Workflow.get_default_workflow().possible_status[:]
543
    workflow.store()
544
    formdef.workflow_id = workflow.id
545
    formdef.workflow_roles = {'_receiver': role.id}
546
    formdef.store()
547
    formdef.data_class().wipe()
548
    formdata = formdef.data_class()()
549
    formdata.data = {
550
        '0': 'foo@localhost',
551
    }
552
    formdata.user_id = user.id
553
    formdata.just_created()
554
    formdata.status = 'wf-new'
555
    formdata.evolution[-1].status = 'wf-new'
556
    formdata.store()
557

  
558
    wfedit = workflow.possible_status[1].add_action('editable', id='_wfedit')
559
    wfedit.by = [user.roles[0]]
560
    workflow.store()
561

  
562
    app.post_json(
563
        '/api/forms/test/%s/' % formdata.id,
564
        {'data': {'0': 'bar@localhost'}},
565
    )
566
    formdata.refresh_from_storage()
567
    # creation
568
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
569
    assert formdata.evolution[-1].parts[0].old_data == {}
570
    assert formdata.evolution[-1].parts[0].new_data == {'0': 'foo@localhost'}
571
    # update
572
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
573
    assert formdata.evolution[-1].parts[1].old_data == {'0': 'foo@localhost'}
574
    assert formdata.evolution[-1].parts[1].new_data == {'0': 'bar@localhost'}
575

  
576

  
577
def test_api_card_import_csv(pub, user, access, role):
578
    app = get_app(pub)
579
    app.set_authorization(('Basic', ('test', '12345')))
580

  
581
    CardDef.wipe()
582
    carddef = CardDef()
583
    carddef.name = 'test'
584
    carddef.fields = [
585
        fields.StringField(id='0', label='foobar', varname='foo'),
586
        fields.StringField(id='1', label='foobar2', varname='foo2'),
587
    ]
588
    carddef.workflow_roles = {'_viewer': role.id}
589
    carddef.backoffice_submission_roles = [role.id]
590
    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
591
    carddef.store()
592

  
593
    carddef.data_class().wipe()
594

  
595
    resp = app.put(
596
        '/api/cards/test/import-csv',
597
        params=b'foobar;foobar2\nfirst entry;plop\n',
598
        headers={'content-type': 'text/csv'},
599
    )
600
    assert resp.json == {'err': 0}
601
    assert carddef.data_class().count() == 1
602
    carddata = carddef.data_class().select()[0]
603
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
604
    assert carddata.evolution[0].parts[0].old_data == {}
605
    assert carddata.evolution[0].parts[0].new_data == {'0': 'first entry', '1': 'plop'}
606

  
607

  
608
def test_api_card_import_json(pub, user, access, role):
609
    app = get_app(pub)
610
    app.set_authorization(('Basic', ('test', '12345')))
611

  
612
    CardDef.wipe()
613
    carddef = CardDef()
614
    carddef.name = 'test'
615
    carddef.fields = [
616
        fields.StringField(id='0', label='foobar', varname='foo'),
617
    ]
618
    carddef.workflow_roles = {'_viewer': role.id}
619
    carddef.backoffice_submission_roles = [role.id]
620
    carddef.store()
621

  
622
    carddef.data_class().wipe()
623

  
624
    data = {
625
        'data': [
626
            {
627
                'fields': {
628
                    'foo': 'bar',
629
                }
630
            },
631
        ]
632
    }
633
    resp = app.put_json(
634
        '/api/cards/test/import-json',
635
        data,
636
    )
637
    assert resp.json == {'err': 0}
638
    assert carddef.data_class().count() == 1
639
    carddata = carddef.data_class().select()[0]
640
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
641
    assert carddata.evolution[0].parts[0].old_data == {}
642
    assert carddata.evolution[0].parts[0].new_data == {'0': 'bar'}
643

  
644

  
645
def test_api_card_submit(pub, user, access, role):
646
    app = get_app(pub)
647
    app.set_authorization(('Basic', ('test', '12345')))
648

  
649
    CardDef.wipe()
650
    carddef = CardDef()
651
    carddef.name = 'test'
652
    carddef.fields = [
653
        fields.StringField(id='1', label='foobar', varname='foobar'),
654
    ]
655
    carddef.backoffice_submission_roles = [role.id]
656
    carddef.store()
657
    data_class = carddef.data_class()
658

  
659
    payload = {
660
        'data': {
661
            'foobar': 'xxx',
662
        }
663
    }
664
    resp = app.post_json(
665
        '/api/cards/test/submit',
666
        payload,
667
    )
668
    assert resp.json['err'] == 0
669
    carddata = data_class.get(resp.json['data']['id'])
670
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
671
    assert carddata.evolution[0].parts[0].old_data == {}
672
    assert carddata.evolution[0].parts[0].new_data == {'1': 'xxx'}
tests/workflow/test_all.py
73 73
from wcs.workflows import (
74 74
    AbortActionException,
75 75
    AttachmentEvolutionPart,
76
    ContentSnapshotPart,
76 77
    Workflow,
77 78
    WorkflowBackofficeFieldsFormDef,
78 79
    WorkflowCriticalityLevel,
......
1343 1344
        assert len(subdir) == 4
1344 1345
        assert len(os.listdir(os.path.join(get_publisher().app_dir, 'attachments', subdir))) == 1
1345 1346

  
1346
    assert len(formdata.evolution[-1].parts) == 2
1347
    assert isinstance(formdata.evolution[-1].parts[0], AttachmentEvolutionPart)
1348
    assert formdata.evolution[-1].parts[0].orig_filename == upload.orig_filename
1347
    assert len(formdata.evolution[-1].parts) == 4
1348
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
1349
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
1349 1350

  
1350
    assert isinstance(formdata.evolution[-1].parts[1], JournalEvolutionPart)
1351
    assert len(formdata.evolution[-1].parts[1].content) > 0
1352
    comment_view = str(formdata.evolution[-1].parts[1].view())
1351
    assert isinstance(formdata.evolution[-1].parts[2], AttachmentEvolutionPart)
1352
    assert formdata.evolution[-1].parts[2].orig_filename == upload.orig_filename
1353

  
1354
    assert isinstance(formdata.evolution[-1].parts[3], JournalEvolutionPart)
1355
    assert len(formdata.evolution[-1].parts[3].content) > 0
1356
    comment_view = str(formdata.evolution[-1].parts[3].view())
1353 1357
    assert comment_view == '<p>%s</p>' % comment_text
1354 1358

  
1355 1359
    if os.path.exists(os.path.join(get_publisher().app_dir, 'attachments')):
......
1422 1426
    register_commenter.comment = 'all'
1423 1427
    register_commenter.to = None
1424 1428
    register_commenter.perform(formdata)
1425
    assert len(formdata.evolution[-1].parts) == 1
1429
    assert len(formdata.evolution[-1].parts) == 2
1426 1430
    assert display_parts() == ['<p>all</p>']
1427 1431

  
1428 1432
    register_commenter.comment = 'to-role'
1429 1433
    register_commenter.to = [role.id]
1430 1434
    register_commenter.perform(formdata)
1431
    assert len(formdata.evolution[-1].parts) == 2
1435
    assert len(formdata.evolution[-1].parts) == 3
1432 1436
    assert len(display_parts()) == 1
1433 1437
    pub._request._user = user
1434 1438
    assert display_parts() == ['<p>all</p>']
......
1439 1443
    register_commenter.comment = 'to-submitter'
1440 1444
    register_commenter.to = ['_submitter']
1441 1445
    register_commenter.perform(formdata)
1442
    assert len(formdata.evolution[-1].parts) == 3
1446
    assert len(formdata.evolution[-1].parts) == 4
1443 1447
    assert display_parts() == ['<p>all</p>']
1444 1448
    formdata.user_id = user.id
1445 1449
    assert display_parts() == ['<p>all</p>', '<p>to-submitter</p>']
......
1447 1451
    register_commenter.comment = 'to-role-or-submitter'
1448 1452
    register_commenter.to = [role.id, '_submitter']
1449 1453
    register_commenter.perform(formdata)
1450
    assert len(formdata.evolution[-1].parts) == 4
1454
    assert len(formdata.evolution[-1].parts) == 5
1451 1455
    assert display_parts() == ['<p>all</p>', '<p>to-submitter</p>', '<p>to-role-or-submitter</p>']
1452 1456
    formdata.user_id = None
1453 1457
    assert display_parts() == ['<p>all</p>']
......
1464 1468
    register_commenter.comment = 'd1'
1465 1469
    register_commenter.to = [role2.id]
1466 1470
    register_commenter.perform(formdata)
1467
    assert len(formdata.evolution[-1].parts) == 5
1471
    assert len(formdata.evolution[-1].parts) == 6
1468 1472
    assert display_parts() == [
1469 1473
        '<p>all</p>',
1470 1474
        '<p>to-role</p>',
......
1476 1480
    register_commenter2.to = [role.id, '_submitter']
1477 1481
    user.roles = [role.id, role2.id]
1478 1482
    register_commenter2.perform(formdata)
1479
    assert len(formdata.evolution[-1].parts) == 6
1483
    assert len(formdata.evolution[-1].parts) == 7
1480 1484
    assert '<p>d1</p>' in [str(x) for x in display_parts()]
1481 1485
    assert '<p>d2</p>' in [str(x) for x in display_parts()]
1482 1486

  
......
1547 1551
    register_commenter.to = [role.id, '_submitter']
1548 1552
    register_commenter.perform(formdata)
1549 1553

  
1550
    assert len(formdata.evolution[-1].parts) == 8
1554
    assert len(formdata.evolution[-1].parts) == 9
1551 1555

  
1552 1556
    assert user.roles == []
1553 1557
    assert len(display_parts()) == 2
......
2192 2196
    assert fbo1.get_content().startswith(b'<?xml')
2193 2197
    # nothing else is stored
2194 2198
    assert formdata.workflow_data is None
2195
    assert not formdata.evolution[-1].parts
2199
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
2196 2200

  
2197 2201
    # store in backoffice file field + varname
2198 2202
    formdata = formdef.data_class()()
......
3673 3677
    item.perform(formdata)
3674 3678

  
3675 3679
    assert formdata.evolution[-1].parts[-1].base_filename == 'template.odt'
3676
    with zipfile.ZipFile(formdata.evolution[-1].parts[0].get_file_path(), mode='r') as zfile:
3680
    with zipfile.ZipFile(formdata.evolution[-1].parts[-1].get_file_path(), mode='r') as zfile:
3677 3681
        zinfo = zfile.getinfo('Pictures/10000000000000320000003276E9D46581B55C88.jpg')
3678 3682
    # check the image has been replaced by the one from the formdata
3679 3683
    assert zinfo.file_size == len(image_data)
......
3688 3692

  
3689 3693
        item.perform(formdata)
3690 3694

  
3691
        with zipfile.ZipFile(formdata.evolution[-1].parts[0].get_file_path(), mode='r') as zfile:
3695
        with zipfile.ZipFile(formdata.evolution[-1].parts[-1].get_file_path(), mode='r') as zfile:
3692 3696
            zinfo = zfile.getinfo('Pictures/10000000000000320000003276E9D46581B55C88.jpg')
3693 3697
        # check the original image has been left
3694 3698
        assert zinfo.file_size == 580
......
3732 3736
    item.perform(formdata)
3733 3737

  
3734 3738
    assert formdata.evolution[-1].parts[-1].base_filename == 'template.odt'
3735
    with zipfile.ZipFile(formdata.evolution[-1].parts[0].get_file_path(), mode='r') as zfile:
3739
    with zipfile.ZipFile(formdata.evolution[-1].parts[-1].get_file_path(), mode='r') as zfile:
3736 3740
        # base template use a jpg images and export_to_model does not rename it
3737 3741
        # event when content is PNG, but it still works inside LibreOffice
3738 3742
        # which ignores the filename extension.
......
3839 3843
    item.convert_to_pdf = False
3840 3844
    item.perform(formdata)
3841 3845

  
3842
    with open(formdata.evolution[0].parts[0].get_file_path(), 'rb') as fd:
3846
    with open(formdata.evolution[0].parts[-1].get_file_path(), 'rb') as fd:
3843 3847
        with zipfile.ZipFile(fd) as zout:
3844 3848
            new_content = zout.read('content.xml')
3845 3849
    assert b'>foo-export-to-template-with-django<' in new_content
......
3848 3852
    formdef.store()
3849 3853
    item.perform(formdata)
3850 3854

  
3851
    with open(formdata.evolution[0].parts[1].get_file_path(), 'rb') as fd:
3855
    with open(formdata.evolution[0].parts[-1].get_file_path(), 'rb') as fd:
3852 3856
        with zipfile.ZipFile(fd) as zout:
3853 3857
            new_content = zout.read('content.xml')
3854 3858
    assert b'>Name with a \' simple quote<' in new_content
......
3857 3861
    formdef.store()
3858 3862
    item.perform(formdata)
3859 3863

  
3860
    with open(formdata.evolution[0].parts[2].get_file_path(), 'rb') as fd:
3864
    with open(formdata.evolution[0].parts[-1].get_file_path(), 'rb') as fd:
3861 3865
        with zipfile.ZipFile(fd) as zout:
3862 3866
            new_content = zout.read('content.xml')
3863 3867
    assert b'>A &lt;&gt; name<' in new_content
......
3994 3998
    item.convert_to_pdf = False
3995 3999
    item.perform(formdata)
3996 4000

  
3997
    with open(formdata.evolution[0].parts[0].get_file_path(), 'rb') as fd:
4001
    with open(formdata.evolution[0].parts[-1].get_file_path(), 'rb') as fd:
3998 4002
        with zipfile.ZipFile(fd) as zout:
3999 4003
            new_content = force_text(zout.read('content.xml'))
4000 4004
    # section content has been removed
......
4018 4022
    assert 'XfooY, Xfoo2Y' in new_content
4019 4023

  
4020 4024
    if filename == 'template-form-details-no-styles.odt':
4021
        with open(formdata.evolution[0].parts[0].get_file_path(), 'rb') as fd:
4025
        with open(formdata.evolution[0].parts[-1].get_file_path(), 'rb') as fd:
4022 4026
            with zipfile.ZipFile(fd) as zout:
4023 4027
                new_styles = force_text(zout.read('styles.xml'))
4024 4028
        assert 'Field_20_Label' in new_styles
......
4964 4968
    # carddef workflow, with global action to increment a counter in its
4965 4969
    # backoffice fields.
4966 4970
    carddef_wf = Workflow(name='Carddef Workflow')
4971
    carddef_wf.add_status('New')
4967 4972
    carddef_wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(carddef_wf)
4968 4973
    carddef_wf.backoffice_fields_formdef.fields = [
4969 4974
        StringField(id='bo0', varname='bo', type='string', label='bo variable'),
......
4988 4993
    # and sample carddata
4989 4994
    carddata = carddef.data_class()()
4990 4995
    carddata.data = {'0': 'Text'}
4996
    carddata.just_created()
4991 4997
    carddata.store()
4992 4998

  
4993 4999
    # formdef workflow that will trigger the global action
......
5048 5054

  
5049 5055
    # carddef workflow, with global action to set a value in a backoffice field
5050 5056
    carddef_wf = Workflow(name='Carddef Workflow')
5057
    carddef_wf.add_status('New')
5051 5058
    carddef_wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(carddef_wf)
5052 5059
    carddef_wf.backoffice_fields_formdef.fields = [
5053 5060
        StringField(id='bo0', varname='bo', type='string', label='bo variable'),
......
5072 5079
    # and sample carddata
5073 5080
    carddata = carddef.data_class()()
5074 5081
    carddata.data = {'0': 'Text'}
5082
    carddata.just_created()
5075 5083
    carddata.store()
5076 5084

  
5077 5085
    # formdef workflow that will trigger the global action
......
5149 5157
    for i in range(1, 4):
5150 5158
        carddata = carddef.data_class()()
5151 5159
        carddata.data = {'0': 'Text %s' % i}
5160
        carddata.just_created()
5152 5161
        carddata.store()
5153 5162

  
5154 5163
    # formdef workflow that will trigger the global action
wcs/api.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import copy
17 18
import datetime
18 19
import json
19 20
import re
......
40 41
from wcs.qommon import get_cfg
41 42
from wcs.qommon.afterjobs import AfterJob
42 43
from wcs.roles import logged_users_role
44
from wcs.workflows import ContentSnapshotPart
43 45

  
44 46
from .backoffice.data_management import CardPage as BackofficeCardPage
45 47
from .backoffice.management import FormPage as BackofficeFormPage
......
174 176
            if 'data' not in json_input:
175 177
                raise RequestError('missing data entry in payload')
176 178
            data = posted_json_data_to_formdata_data(self.formdef, json_input['data'])
179
            old_data = copy.deepcopy(self.formdata.data)
177 180
            self.formdata.data.update(data)
178 181
            self.formdata.store()
179 182

  
180 183
            if item.status:
181 184
                self.formdata.jump_status(item.status)
182 185
                self.formdata.perform_workflow(event=('api-post-edit-action', item.id))
186
            evo = self.formdata.evolution[-1]
187
            evo.add_part(ContentSnapshotPart(old_data=old_data, new_data=copy.deepcopy(self.formdata.data)))
188
            self.formdata.store()
183 189

  
184 190
            return json.dumps({'err': 0, 'data': {'id': str(self.formdata.id)}})
185 191

  
wcs/formdata.py
492 492
            return None
493 493

  
494 494
    def just_created(self):
495
        from wcs.workflows import ContentSnapshotPart
496

  
495 497
        self.receipt_time = time.localtime()
496 498
        self.status = 'wf-%s' % self.formdef.workflow.possible_status[0].id
497 499
        # we add the initial status to the history, this makes it more readable
......
502 504
        evo.time = self.receipt_time
503 505
        evo.status = self.status
504 506
        self.evolution = [evo]
507
        evo.add_part(ContentSnapshotPart(old_data={}, new_data=copy.deepcopy(self.data)))
505 508

  
506 509
    def set_auto_fields(self, *args, **kwargs):
507 510
        fields = {}
......
760 763
            return None
761 764

  
762 765
    def jump_status(self, status_id, user_id=None):
763
        from wcs.workflows import ActionsTracingEvolutionPart
766
        from wcs.workflows import ActionsTracingEvolutionPart, ContentSnapshotPart
764 767

  
765 768
        if status_id == '_previous':
766 769
            previous_status = self.pop_previous_marked_status()
......
777 780
            and self.evolution[-1].status == status
778 781
            and not self.evolution[-1].comment
779 782
            and not [
780
                x for x in self.evolution[-1].parts or [] if not isinstance(x, ActionsTracingEvolutionPart)
783
                x
784
                for x in self.evolution[-1].parts or []
785
                if not isinstance(x, (ActionsTracingEvolutionPart, ContentSnapshotPart))
781 786
            ]
782 787
        ):
783 788
            # if status do not change and last evolution is empty,
wcs/forms/root.py
43 43
from wcs.qommon.storage import Equal, NothingToUpdate
44 44
from wcs.roles import logged_users_role
45 45
from wcs.variables import LazyFormDef
46
from wcs.workflows import Workflow, WorkflowBackofficeFieldsFormDef, WorkflowStatusItem
46
from wcs.workflows import ContentSnapshotPart, Workflow, WorkflowBackofficeFieldsFormDef, WorkflowStatusItem
47 47

  
48 48
from ..qommon import _, emails, errors, get_cfg, misc, ngettext, template
49 49
from ..qommon.admin.emails import EmailsDirectory
......
1641 1641
        for k, v in self.edited_data.data.items():
1642 1642
            if k.startswith(WorkflowBackofficeFieldsFormDef.field_prefix):
1643 1643
                new_data[k] = v
1644
        old_data = copy.deepcopy(self.edited_data.data)
1644 1645
        self.edited_data.data = new_data
1645 1646
        if getattr(self, 'selected_user_id', None):
1646 1647
            # user selection in backoffice
1647 1648
            self.edited_data.user_id = self.selected_user_id
1649
        evo = self.edited_data.evolution[-1]
1650
        evo.add_part(ContentSnapshotPart(old_data=old_data, new_data=copy.deepcopy(self.edited_data.data)))
1648 1651
        self.edited_data.store()
1649 1652
        # remove previous vars and formdata from substitution variables
1650 1653
        self.clean_submission_context()
wcs/wf/backoffice_fields.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import copy
17 18
import xml.etree.ElementTree as ET
18 19

  
19 20
from quixote import get_publisher
......
21 22

  
22 23
from wcs.fields import SetValueError, WidgetField
23 24
from wcs.wf.profile import FieldNode
24
from wcs.workflows import WorkflowStatusItem, register_item_class
25
from wcs.workflows import ContentSnapshotPart, WorkflowStatusItem, register_item_class
25 26

  
26 27
from ..qommon import _
27 28
from ..qommon.form import (
......
137 138
    def perform(self, formdata):
138 139
        if not self.fields:
139 140
            return
141

  
142
        old_data = copy.deepcopy(formdata.data)
140 143
        for field in self.fields:
141 144
            try:
142 145
                formdef_field = [
......
203 206
            # and backoffice field values can be used in subsequent fields.
204 207
            formdata.store()
205 208

  
209
        evo = formdata.evolution[-1]
210
        evo.add_part(ContentSnapshotPart(old_data=old_data, new_data=copy.deepcopy(formdata.data)))
211
        formdata.store()
212

  
206 213
    def fields_export_to_xml(self, item, charset, include_id=False):
207 214
        if not self.fields:
208 215
            return
wcs/wf/edit_carddata.py
14 14
# You should have received a copy of the GNU General Public License
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import copy
18
import time
19

  
17 20
from quixote import get_publisher
18 21

  
22
from wcs.formdata import Evolution
19 23
from wcs.qommon import _
20 24
from wcs.wf.create_carddata import CreateCarddataWorkflowStatusItem
21 25
from wcs.wf.external_workflow import ExternalWorkflowGlobalAction
22
from wcs.workflows import register_item_class
26
from wcs.workflows import ContentSnapshotPart, register_item_class
23 27

  
24 28

  
25 29
class EditCarddataWorkflowStatusItem(CreateCarddataWorkflowStatusItem, ExternalWorkflowGlobalAction):
......
49 53
        formdata.store()
50 54

  
51 55
        for target_data in self.iter_target_datas(formdata, carddef):
56
            old_data = copy.deepcopy(target_data.data)
52 57
            self.apply_mappings(dest=target_data, src=formdata)
53 58
            with get_publisher().substitutions.freeze():
59
                evo = Evolution()
60
                evo.time = time.localtime()
61
                evo.status = target_data.status
62
                target_data.evolution.append(evo)
63
                evo.add_part(ContentSnapshotPart(old_data=old_data, new_data=copy.deepcopy(target_data.data)))
54 64
                target_data.store()
55 65

  
56 66
        # update local object as it may have modified itself
wcs/workflows.py
452 452
        return real_action
453 453

  
454 454

  
455
class ContentSnapshotPart(EvolutionPart):
456
    def __init__(self, old_data, new_data):
457
        self.old_data = old_data
458
        self.new_data = new_data
459

  
460

  
455 461
class DuplicateGlobalActionNameError(Exception):
456 462
    pass
457 463

  
458
-