Projet

Général

Profil

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

Lauréline Guérin, 21 octobre 2022 12:11

Télécharger (50,2 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   | 741 ++++++++++++++++++++++++++++++
 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                  |  11 +
 10 files changed, 833 insertions(+), 39 deletions(-)
 create mode 100644 tests/test_content_snapshots.py
tests/form_pages/test_all.py
9129 9129
        '1_display': 'un',
9130 9130
        '1_structured': {'id': '1', 'text': 'un', 'more': 'foo'},
9131 9131
    }
9132
    assert '2020-04-18' in formdata.evolution[0].parts[0].content
9132
    assert '2020-04-18' in formdata.evolution[0].parts[1].content
9133 9133

  
9134 9134

  
9135 9135
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):
......
1456 1458
    assert 'The form has been recorded and: XbarY' in resp.text
1457 1459

  
1458 1460
    formdata = formdef.data_class().select()[0]
1459
    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
1461
    assert formdata.evolution[0].parts[1].content == 'Hello bar World'
1460 1462

  
1461 1463
    # check with publisher variable in named webservice call
1462 1464
    if not pub.site_options.has_section('variables'):
......
1481 1483
    assert 'The form has been recorded and: XbarY' in resp.text
1482 1484

  
1483 1485
    formdata = formdef.data_class().select()[0]
1484
    assert formdata.evolution[0].parts[0].content == 'Hello bar World'
1486
    assert formdata.evolution[0].parts[1].content == 'Hello bar World'
1485 1487

  
1486 1488

  
1487 1489
def test_formdata_named_wscall_in_conditions(http_requests, pub):
......
1607 1609
    assert 'The form has been recorded' in resp.text
1608 1610

  
1609 1611
    formdata = formdef.data_class().select()[0]
1610
    assert formdata.evolution[0].parts[0].content == 'Hello World'
1611
    assert formdata.evolution[0].parts[0].to is None
1612
    assert formdata.evolution[0].parts[1].content == 'Hello World'
1613
    assert formdata.evolution[0].parts[1].to is None
1612 1614
    resp = app.get('/test/%s/' % formdata.id)
1613 1615
    resp.status_int = 200
1614 1616
    assert resp.html.find('div', {'id': 'evolution-log'}).find('p').text == 'Hello World'
......
1627 1629
    assert 'The form has been recorded' in resp.text
1628 1630

  
1629 1631
    formdata = formdef.data_class().select()[0]
1630
    assert formdata.evolution[0].parts[0].content == 'Hello World'
1631
    assert formdata.evolution[0].parts[0].to == [role1.id]
1632
    assert formdata.evolution[0].parts[1].content == 'Hello World'
1633
    assert formdata.evolution[0].parts[1].to == [role1.id]
1632 1634
    resp = app.get('/test/%s/' % formdata.id)
1633 1635
    resp.status_int = 200
1634 1636
    assert not resp.html.find('div', {'id': 'evolution-log'}).find('p')
......
1647 1649
    assert 'The form has been recorded' in resp.text
1648 1650

  
1649 1651
    formdata = formdef.data_class().select()[0]
1650
    assert formdata.evolution[0].parts[0].content == 'Hello World'
1651
    assert formdata.evolution[0].parts[0].to == [role2.id]
1652
    assert formdata.evolution[0].parts[1].content == 'Hello World'
1653
    assert formdata.evolution[0].parts[1].to == [role2.id]
1652 1654
    resp = app.get('/test/%s/' % formdata.id)
1653 1655
    resp.status_int = 200
1654 1656
    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].formdef_type == 'formdef'
138
    assert formdata.evolution[0].parts[0].formdef_id == str(formdef.id)
139
    assert formdata.evolution[0].parts[0].old_data == {}
140
    assert formdata.evolution[0].parts[0].new_data == {'1': 'bar', 'bo1': None}
141
    dt1 = formdata.evolution[0].parts[0].datetime
142
    # creation, bo field first action
143
    assert formdata.evolution[0].parts[1].formdef_type == 'formdef'
144
    assert formdata.evolution[0].parts[1].formdef_id == str(formdef.id)
145
    assert formdata.evolution[0].parts[1].old_data == {'1': 'bar'}
146
    assert formdata.evolution[0].parts[1].new_data == {'1': 'bar', 'bo1': 'bar'}
147
    dt2 = formdata.evolution[0].parts[1].datetime
148
    assert dt2 > dt1
149
    # creation, bo field second action
150
    assert formdata.evolution[0].parts[2].formdef_type == 'formdef'
151
    assert formdata.evolution[0].parts[2].formdef_id == str(formdef.id)
152
    assert formdata.evolution[0].parts[2].old_data == {'1': 'bar', 'bo1': 'bar'}
153
    assert formdata.evolution[0].parts[2].new_data == {'1': 'bar', 'bo1': 'foobar'}
154
    dt3 = formdata.evolution[0].parts[2].datetime
155
    assert dt3 > dt2
156
    # update, submit
157
    assert formdata.evolution[1].parts[0].formdef_type == 'formdef'
158
    assert formdata.evolution[1].parts[0].formdef_id == str(formdef.id)
159
    assert formdata.evolution[1].parts[0].old_data == {'1': 'bar', 'bo1': 'foobar'}
160
    assert formdata.evolution[1].parts[0].new_data == {'1': 'baz', 'bo1': 'foobar'}
161
    dt4 = formdata.evolution[1].parts[0].datetime
162
    assert dt4 > dt3
163
    # update, bo field first action
164
    assert formdata.evolution[2].parts[0].formdef_type == 'formdef'
165
    assert formdata.evolution[2].parts[0].formdef_id == str(formdef.id)
166
    assert formdata.evolution[2].parts[0].old_data == {'1': 'baz', 'bo1': 'foobar'}
167
    assert formdata.evolution[2].parts[0].new_data == {'1': 'baz', 'bo1': 'baz'}
168
    dt5 = formdata.evolution[2].parts[0].datetime
169
    assert dt5 > dt4
170
    # update, bo field second action
171
    assert formdata.evolution[2].parts[1].formdef_type == 'formdef'
172
    assert formdata.evolution[2].parts[1].formdef_id == str(formdef.id)
173
    assert formdata.evolution[2].parts[1].old_data == {'1': 'baz', 'bo1': 'baz'}
174
    assert formdata.evolution[2].parts[1].new_data == {'1': 'baz', 'bo1': 'foobaz'}
175
    dt6 = formdata.evolution[2].parts[1].datetime
176
    assert dt6 > dt5
177

  
178

  
179
def test_backoffice_formdata_submission(pub, user):
180
    FormDef.wipe()
181
    formdef = FormDef()
182
    formdef.name = 'form title'
183
    formdef.fields = [
184
        fields.StringField(id='1', label='String', type='string'),
185
    ]
186
    formdef.workflow_roles = {'_receiver': user.roles[0]}
187
    formdef.backoffice_submission_roles = user.roles[:]
188
    formdef.store()
189

  
190
    app = login(get_app(pub))
191
    resp = app.get('/backoffice/submission/form-title/')
192
    resp.form['f1'] = 'test submission'
193
    resp = resp.form.submit('submit')  # -> validation
194
    resp = resp.form.submit('submit').follow()  # -> submitted
195
    assert 'The form has been recorded' in resp.text
196

  
197
    formdata = formdef.data_class().select()[0]
198
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
199
    assert formdata.evolution[0].parts[0].formdef_type == 'formdef'
200
    assert formdata.evolution[0].parts[0].formdef_id == str(formdef.id)
201
    assert formdata.evolution[0].parts[0].old_data == {}
202
    assert formdata.evolution[0].parts[0].new_data == {'1': 'test submission'}
203

  
204

  
205
def test_backoffice_carddata_add_edit(pub, user):
206
    CardDef.wipe()
207
    carddef = CardDef()
208
    carddef.name = 'test'
209
    carddef.fields = [
210
        fields.StringField(id='1', label='string', varname='foo'),
211
    ]
212
    carddef.backoffice_submission_roles = user.roles
213
    carddef.workflow_roles = {
214
        '_viewer': user.roles[0],
215
        '_editor': user.roles[0],
216
    }
217
    carddef.store()
218
    carddef.data_class().wipe()
219

  
220
    Workflow.wipe()
221
    workflow = Workflow(name='test')
222
    workflow.roles = {
223
        '_viewer': 'Viewer',
224
        '_editor': 'Editor',
225
    }
226
    st1 = workflow.add_status('Status1', 'st1')
227
    jump = st1.add_action('jump')
228
    jump.status = 'st2'
229
    st2 = workflow.add_status('Status2', 'st2')
230
    editable = st2.add_action('editable', id='_editable')
231
    editable.by = ['_editor']
232
    editable.status = st1.id
233
    workflow.store()
234

  
235
    carddef.workflow_id = workflow.id
236
    carddef.store()
237
    carddef.data_class().wipe()
238

  
239
    app = login(get_app(pub))
240
    resp = app.get('/backoffice/data/test/add/')
241
    resp.form['f1'] = 'foo'
242
    resp = resp.form.submit('submit').follow()
243
    assert 'button_editable-button' in resp.text
244

  
245
    resp = resp.form.submit('button_editable')
246
    resp = resp.follow()
247
    resp.form['f1'].value = 'bar'
248
    resp = resp.form.submit('submit').follow()
249

  
250
    carddata = carddef.data_class().select()[0]
251
    # creation
252
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
253
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
254
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
255
    assert carddata.evolution[0].parts[0].old_data == {}
256
    assert carddata.evolution[0].parts[0].new_data == {'1': 'foo'}
257
    dt1 = carddata.evolution[0].parts[0].datetime
258
    # update
259
    assert isinstance(carddata.evolution[1].parts[0], ContentSnapshotPart)
260
    assert carddata.evolution[1].parts[0].formdef_type == 'carddef'
261
    assert carddata.evolution[1].parts[0].formdef_id == str(carddef.id)
262
    assert carddata.evolution[1].parts[0].old_data == {'1': 'foo'}
263
    assert carddata.evolution[1].parts[0].new_data == {'1': 'bar'}
264
    dt2 = carddata.evolution[1].parts[0].datetime
265
    assert dt2 > dt1
266

  
267

  
268
def test_backoffice_card_import_csv(pub, user):
269
    CardDef.wipe()
270
    carddef = CardDef()
271
    carddef.name = 'test'
272
    carddef.fields = [
273
        fields.StringField(id='1', label='Test'),
274
        fields.StringField(id='2', label='Test2'),
275
    ]
276
    carddef.workflow_roles = {'_editor': user.roles[0]}
277
    carddef.backoffice_submission_roles = user.roles
278
    carddef.store()
279
    carddef.data_class().wipe()
280

  
281
    app = login(get_app(pub))
282

  
283
    resp = app.get(carddef.get_url())
284
    resp = resp.click('Import data from a file')
285
    data = [b'Test,Test2']
286
    data.append(b'data,foo')
287

  
288
    resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), 'text/csv')
289
    resp = resp.forms[0].submit().follow()
290
    assert 'Importing data into cards' in resp
291
    assert carddef.data_class().count() == 1
292
    carddata = carddef.data_class().select()[0]
293
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
294
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
295
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
296
    assert carddata.evolution[0].parts[0].old_data == {}
297
    assert carddata.evolution[0].parts[0].new_data == {'1': 'data', '2': 'foo'}
298

  
299

  
300
def test_backoffice_card_import_json(pub, user):
301
    CardDef.wipe()
302
    carddef = CardDef()
303
    carddef.name = 'test'
304
    carddef.fields = [
305
        fields.StringField(id='1', label='Test', varname='string'),
306
    ]
307
    carddef.workflow_roles = {'_editor': user.roles[0]}
308
    carddef.backoffice_submission_roles = user.roles
309
    carddef.store()
310
    carddef.data_class().wipe()
311

  
312
    app = login(get_app(pub))
313
    resp = app.get(carddef.get_url())
314
    resp = resp.click('Import data from a file')
315
    data = {
316
        'data': [
317
            {
318
                'fields': {
319
                    'string': 'a string',
320
                }
321
            }
322
        ]
323
    }
324
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
325
    resp = resp.forms[0].submit()
326
    assert '/backoffice/processing?job=' in resp.location
327
    resp = resp.follow()
328

  
329
    carddata = carddef.data_class().select()[0]
330
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
331
    assert carddata.evolution[-1].parts[0].formdef_type == 'carddef'
332
    assert carddata.evolution[-1].parts[0].formdef_id == str(carddef.id)
333
    assert carddata.evolution[-1].parts[0].old_data == {}
334
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'a string'}
335

  
336

  
337
def test_workflow_formdata_create(pub):
338
    FormDef.wipe()
339
    pub.tracking_code_class.wipe()
340

  
341
    target_formdef = FormDef()
342
    target_formdef.name = 'target form'
343
    target_formdef.fields = [
344
        fields.StringField(id='0', label='string', varname='string'),
345
    ]
346
    target_formdef.store()
347

  
348
    wf = Workflow(name='create-formdata')
349
    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
350
    create = wf.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
351
    create.formdef_slug = target_formdef.url_name
352
    create.label = 'create a new linked form'
353
    create.varname = 'resubmitted'
354
    create.mappings = [
355
        Mapping(field_id='0', expression='{{ form_var_foo }}'),
356
    ]
357
    wf.store()
358

  
359
    source_formdef = FormDef()
360
    source_formdef.name = 'source form'
361
    source_formdef.fields = [
362
        fields.StringField(id='0', label='string', varname='foo'),
363
    ]
364
    source_formdef.workflow_id = wf.id
365
    source_formdef.store()
366

  
367
    formdata = source_formdef.data_class()()
368
    formdata.data = {'0': 'foobar'}
369
    formdata.just_created()
370
    formdata.store()
371

  
372
    formdata.perform_workflow()
373
    assert target_formdef.data_class().count() == 1
374
    created_formdata = target_formdef.data_class().select()[0]
375
    assert isinstance(created_formdata.evolution[0].parts[0], ContentSnapshotPart)
376
    assert created_formdata.evolution[0].parts[0].formdef_type == 'formdef'
377
    assert created_formdata.evolution[0].parts[0].formdef_id == str(target_formdef.id)
378
    assert created_formdata.evolution[0].parts[0].old_data == {}
379
    assert created_formdata.evolution[0].parts[0].new_data == {'0': 'foobar'}
380

  
381

  
382
def test_workflow_carddata_create(pub):
383
    CardDef.wipe()
384
    FormDef.wipe()
385

  
386
    carddef = CardDef()
387
    carddef.name = 'My card'
388
    carddef.fields = [
389
        fields.StringField(id='1', label='string'),
390
    ]
391
    carddef.store()
392

  
393
    wf = Workflow(name='create-carddata')
394
    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
395
    create = wf.possible_status[1].add_action('create_carddata', id='_create', prepend=True)
396
    create.label = 'Create CardDef'
397
    create.varname = 'mycard'
398
    create.formdef_slug = carddef.url_name
399
    create.mappings = [
400
        Mapping(field_id='1', expression='{{ form_var_string }}'),
401
    ]
402
    wf.store()
403

  
404
    formdef = FormDef()
405
    formdef.name = 'source form'
406
    formdef.fields = [
407
        fields.StringField(id='1', label='string', varname='string'),
408
    ]
409
    formdef.workflow_id = wf.id
410
    formdef.store()
411

  
412
    formdata = formdef.data_class()()
413
    formdata.data = {'1': 'foobar'}
414
    formdata.just_created()
415
    formdata.store()
416

  
417
    formdata.perform_workflow()
418
    assert carddef.data_class().count() == 1
419
    carddata = carddef.data_class().select()[0]
420
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
421
    assert carddata.evolution[-1].parts[0].formdef_type == 'carddef'
422
    assert carddata.evolution[-1].parts[0].formdef_id == str(carddef.id)
423
    assert carddata.evolution[-1].parts[0].old_data == {}
424
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'foobar'}
425

  
426

  
427
def test_workflow_carddata_edit(pub):
428
    FormDef.wipe()
429
    CardDef.wipe()
430

  
431
    # carddef
432
    carddef = CardDef()
433
    carddef.name = 'Model 1'
434
    carddef.fields = [
435
        fields.StringField(id='1', label='string', varname='string'),
436
    ]
437
    carddef.store()
438
    carddef.data_class().wipe()
439

  
440
    carddata = carddef.data_class()()
441
    carddata.data = {
442
        '1': 'foobar',
443
    }
444
    carddata.just_created()
445
    carddata.store()
446

  
447
    # formdef workflow that will update carddata
448
    wf = Workflow(name='update')
449
    st1 = wf.add_status('New', 'st1')
450
    jump = st1.add_action('jump', id='_jump')
451
    jump.by = ['_submitter', '_receiver']
452
    jump.status = 'st2'
453
    st2 = wf.add_status('Update card', 'st2')
454
    edit = st2.add_action('edit_carddata', id='edit')
455
    edit.formdef_slug = carddef.url_name
456
    edit.target_mode = 'manual'
457
    edit.target_id = '{{ form_var_card }}'
458
    edit.mappings = [
459
        Mapping(field_id='1', expression='{{ form_var_foo }}'),
460
    ]
461
    wf.store()
462

  
463
    # associated formdef
464
    formdef = FormDef()
465
    formdef.name = 'Update'
466
    formdef.fields = [
467
        fields.StringField(id='1', label='string', varname='card'),
468
        fields.StringField(id='2', label='string', varname='foo'),
469
    ]
470
    formdef.workflow = wf
471
    formdef.store()
472

  
473
    formdata = formdef.data_class()()
474
    formdata.data = {
475
        '1': str(carddata.id),
476
        '2': 'foobaz',
477
    }
478
    formdata.just_created()
479
    formdata.store()
480
    formdata.perform_workflow()
481
    carddata.refresh_from_storage()
482
    # creation
483
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
484
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
485
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
486
    assert carddata.evolution[0].parts[0].old_data == {}
487
    assert carddata.evolution[0].parts[0].new_data == {'1': 'foobar'}
488
    dt1 = carddata.evolution[0].parts[0].datetime
489
    # update
490
    assert isinstance(carddata.evolution[1].parts[0], ContentSnapshotPart)
491
    assert carddata.evolution[1].parts[0].formdef_type == 'carddef'
492
    assert carddata.evolution[1].parts[0].formdef_id == str(carddef.id)
493
    assert carddata.evolution[1].parts[0].old_data == {'1': 'foobar'}
494
    assert carddata.evolution[1].parts[0].new_data == {'1': 'foobaz'}
495
    dt2 = carddata.evolution[1].parts[0].datetime
496
    assert dt2 > dt1
497

  
498

  
499
def test_workflow_set_backoffice_field(http_requests, pub):
500
    Workflow.wipe()
501
    FormDef.wipe()
502
    wf = Workflow(name='xxx')
503
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
504
    wf.backoffice_fields_formdef.fields = [
505
        fields.StringField(id='bo1', label='1st backoffice field', type='string', varname='backoffice_blah'),
506
    ]
507
    st1 = wf.add_status('Status1')
508
    wf.store()
509

  
510
    formdef = FormDef()
511
    formdef.name = 'baz'
512
    formdef.fields = [
513
        fields.StringField(id='0', label='String', type='string', varname='string'),
514
    ]
515
    formdef.workflow_id = wf.id
516
    formdef.store()
517

  
518
    formdata = formdef.data_class()()
519
    formdata.data = {'0': 'HELLO'}
520
    formdata.just_created()
521
    formdata.store()
522
    pub.substitutions.feed(formdata)
523

  
524
    item = SetBackofficeFieldsWorkflowStatusItem()
525
    item.fields = [{'field_id': 'bo1', 'value': '{{ form_var_string }}'}]
526
    item.parent = st1
527

  
528
    item.perform(formdata)
529
    formdata.refresh_from_storage()
530
    # creation
531
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
532
    assert formdata.evolution[-1].parts[0].formdef_type == 'formdef'
533
    assert formdata.evolution[-1].parts[0].formdef_id == str(formdef.id)
534
    assert formdata.evolution[-1].parts[0].old_data == {}
535
    assert formdata.evolution[-1].parts[0].new_data == {
536
        '0': 'HELLO',
537
    }
538
    dt1 = formdata.evolution[-1].parts[0].datetime
539
    # bo action
540
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
541
    assert formdata.evolution[-1].parts[1].formdef_type == 'formdef'
542
    assert formdata.evolution[-1].parts[1].formdef_id == str(formdef.id)
543
    assert formdata.evolution[-1].parts[1].old_data == {
544
        '0': 'HELLO',
545
    }
546
    assert formdata.evolution[-1].parts[1].new_data == {
547
        '0': 'HELLO',
548
        'bo1': 'HELLO',
549
    }
550
    dt2 = formdata.evolution[-1].parts[1].datetime
551
    assert dt2 > dt1
552

  
553

  
554
def test_api_form_submit(pub, user, access, role):
555
    app = get_app(pub)
556
    app.set_authorization(('Basic', ('test', '12345')))
557

  
558
    FormDef.wipe()
559
    formdef = FormDef()
560
    formdef.name = 'test'
561
    formdef.fields = [
562
        fields.StringField(id='1', label='foobar', varname='foobar'),
563
    ]
564
    formdef.backoffice_submission_roles = [role.id]
565
    formdef.store()
566
    data_class = formdef.data_class()
567

  
568
    payload = {
569
        'data': {
570
            'foobar': 'xxx',
571
        }
572
    }
573
    resp = app.post_json(
574
        '/api/formdefs/test/submit',
575
        payload,
576
    )
577
    assert resp.json['err'] == 0
578
    formdata = data_class.get(resp.json['data']['id'])
579
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
580
    assert formdata.evolution[0].parts[0].formdef_type == 'formdef'
581
    assert formdata.evolution[0].parts[0].formdef_id == str(formdef.id)
582
    assert formdata.evolution[0].parts[0].old_data == {}
583
    assert formdata.evolution[0].parts[0].new_data == {'1': 'xxx'}
584

  
585

  
586
def test_api_formdata_edit(pub, user, access, role):
587
    app = get_app(pub)
588
    app.set_authorization(('Basic', ('test', '12345')))
589

  
590
    FormDef.wipe()
591
    formdef = FormDef()
592
    formdef.name = 'test'
593
    formdef.fields = [
594
        fields.StringField(id='0', label='foobar', varname='foobar'),
595
    ]
596
    Workflow.wipe()
597
    workflow = Workflow(name='foo')
598
    workflow.possible_status = Workflow.get_default_workflow().possible_status[:]
599
    workflow.store()
600
    formdef.workflow_id = workflow.id
601
    formdef.workflow_roles = {'_receiver': role.id}
602
    formdef.store()
603
    formdef.data_class().wipe()
604
    formdata = formdef.data_class()()
605
    formdata.data = {
606
        '0': 'foo@localhost',
607
    }
608
    formdata.user_id = user.id
609
    formdata.just_created()
610
    formdata.status = 'wf-new'
611
    formdata.evolution[-1].status = 'wf-new'
612
    formdata.store()
613

  
614
    wfedit = workflow.possible_status[1].add_action('editable', id='_wfedit')
615
    wfedit.by = [user.roles[0]]
616
    workflow.store()
617

  
618
    app.post_json(
619
        '/api/forms/test/%s/' % formdata.id,
620
        {'data': {'0': 'bar@localhost'}},
621
    )
622
    formdata.refresh_from_storage()
623
    # creation
624
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
625
    assert formdata.evolution[-1].parts[0].formdef_type == 'formdef'
626
    assert formdata.evolution[-1].parts[0].formdef_id == str(formdef.id)
627
    assert formdata.evolution[-1].parts[0].old_data == {}
628
    assert formdata.evolution[-1].parts[0].new_data == {'0': 'foo@localhost'}
629
    dt1 = formdata.evolution[-1].parts[0].datetime
630
    # update
631
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
632
    assert formdata.evolution[-1].parts[1].formdef_type == 'formdef'
633
    assert formdata.evolution[-1].parts[1].formdef_id == str(formdef.id)
634
    assert formdata.evolution[-1].parts[1].old_data == {'0': 'foo@localhost'}
635
    assert formdata.evolution[-1].parts[1].new_data == {'0': 'bar@localhost'}
636
    dt2 = formdata.evolution[-1].parts[1].datetime
637
    assert dt2 > dt1
638

  
639

  
640
def test_api_card_import_csv(pub, user, access, role):
641
    app = get_app(pub)
642
    app.set_authorization(('Basic', ('test', '12345')))
643

  
644
    CardDef.wipe()
645
    carddef = CardDef()
646
    carddef.name = 'test'
647
    carddef.fields = [
648
        fields.StringField(id='0', label='foobar', varname='foo'),
649
        fields.StringField(id='1', label='foobar2', varname='foo2'),
650
    ]
651
    carddef.workflow_roles = {'_viewer': role.id}
652
    carddef.backoffice_submission_roles = [role.id]
653
    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
654
    carddef.store()
655

  
656
    carddef.data_class().wipe()
657

  
658
    resp = app.put(
659
        '/api/cards/test/import-csv',
660
        params=b'foobar;foobar2\nfirst entry;plop\n',
661
        headers={'content-type': 'text/csv'},
662
    )
663
    assert resp.json == {'err': 0}
664
    assert carddef.data_class().count() == 1
665
    carddata = carddef.data_class().select()[0]
666
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
667
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
668
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
669
    assert carddata.evolution[0].parts[0].old_data == {}
670
    assert carddata.evolution[0].parts[0].new_data == {'0': 'first entry', '1': 'plop'}
671

  
672

  
673
def test_api_card_import_json(pub, user, access, role):
674
    app = get_app(pub)
675
    app.set_authorization(('Basic', ('test', '12345')))
676

  
677
    CardDef.wipe()
678
    carddef = CardDef()
679
    carddef.name = 'test'
680
    carddef.fields = [
681
        fields.StringField(id='0', label='foobar', varname='foo'),
682
    ]
683
    carddef.workflow_roles = {'_viewer': role.id}
684
    carddef.backoffice_submission_roles = [role.id]
685
    carddef.store()
686

  
687
    carddef.data_class().wipe()
688

  
689
    data = {
690
        'data': [
691
            {
692
                'fields': {
693
                    'foo': 'bar',
694
                }
695
            },
696
        ]
697
    }
698
    resp = app.put_json(
699
        '/api/cards/test/import-json',
700
        data,
701
    )
702
    assert resp.json == {'err': 0}
703
    assert carddef.data_class().count() == 1
704
    carddata = carddef.data_class().select()[0]
705
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
706
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
707
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
708
    assert carddata.evolution[0].parts[0].old_data == {}
709
    assert carddata.evolution[0].parts[0].new_data == {'0': 'bar'}
710

  
711

  
712
def test_api_card_submit(pub, user, access, role):
713
    app = get_app(pub)
714
    app.set_authorization(('Basic', ('test', '12345')))
715

  
716
    CardDef.wipe()
717
    carddef = CardDef()
718
    carddef.name = 'test'
719
    carddef.fields = [
720
        fields.StringField(id='1', label='foobar', varname='foobar'),
721
    ]
722
    carddef.backoffice_submission_roles = [role.id]
723
    carddef.store()
724
    data_class = carddef.data_class()
725

  
726
    payload = {
727
        'data': {
728
            'foobar': 'xxx',
729
        }
730
    }
731
    resp = app.post_json(
732
        '/api/cards/test/submit',
733
        payload,
734
    )
735
    assert resp.json['err'] == 0
736
    carddata = data_class.get(resp.json['data']['id'])
737
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
738
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
739
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
740
    assert carddata.evolution[0].parts[0].old_data == {}
741
    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(formdata=self.formdata, old_data=old_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(formdata=self, old_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
......
1647 1647
        for k, v in self.edited_data.data.items():
1648 1648
            if k.startswith(WorkflowBackofficeFieldsFormDef.field_prefix):
1649 1649
                new_data[k] = v
1650
        old_data = copy.deepcopy(self.edited_data.data)
1650 1651
        self.edited_data.data = new_data
1651 1652
        if getattr(self, 'selected_user_id', None):
1652 1653
            # user selection in backoffice
1653 1654
            self.edited_data.user_id = self.selected_user_id
1655
        evo = self.edited_data.evolution[-1]
1656
        evo.add_part(ContentSnapshotPart(formdata=self.edited_data, old_data=old_data))
1654 1657
        self.edited_data.store()
1655 1658
        # remove previous vars and formdata from substitution variables
1656 1659
        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(formdata=formdata, old_data=old_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(formdata=target_data, old_data=old_data))
54 64
                target_data.store()
55 65

  
56 66
        # update local object as it may have modified itself
wcs/workflows.py
16 16

  
17 17
import base64
18 18
import collections
19
import copy
19 20
import datetime
20 21
import glob
21 22
import itertools
......
27 28
from importlib import import_module
28 29

  
29 30
from django.utils.encoding import force_text
31
from django.utils.timezone import now
30 32
from quixote import get_publisher, get_request, get_response
31 33
from quixote.html import TemplateIO, htmltext
32 34

  
......
452 454
        return real_action
453 455

  
454 456

  
457
class ContentSnapshotPart(EvolutionPart):
458
    def __init__(self, formdata, old_data):
459
        self.datetime = now()
460
        self.formdef_type = formdata.formdef.xml_root_node
461
        self.formdef_id = formdata.formdef.id
462
        self.old_data = old_data
463
        self.new_data = copy.deepcopy(formdata.data)
464

  
465

  
455 466
class DuplicateGlobalActionNameError(Exception):
456 467
    pass
457 468

  
458
-