Projet

Général

Profil

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

Lauréline Guérin, 20 octobre 2022 11:01

Télécharger (49 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   | 718 ++++++++++++++++++++++++++++++
 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                  |   9 +
 10 files changed, 808 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
    # creation, bo field first action
142
    assert formdata.evolution[0].parts[1].formdef_type == 'formdef'
143
    assert formdata.evolution[0].parts[1].formdef_id == str(formdef.id)
144
    assert formdata.evolution[0].parts[1].old_data == {'1': 'bar'}
145
    assert formdata.evolution[0].parts[1].new_data == {'1': 'bar', 'bo1': 'bar'}
146
    # creation, bo field second action
147
    assert formdata.evolution[0].parts[2].formdef_type == 'formdef'
148
    assert formdata.evolution[0].parts[2].formdef_id == str(formdef.id)
149
    assert formdata.evolution[0].parts[2].old_data == {'1': 'bar', 'bo1': 'bar'}
150
    assert formdata.evolution[0].parts[2].new_data == {'1': 'bar', 'bo1': 'foobar'}
151
    # update, submit
152
    assert formdata.evolution[1].parts[0].formdef_type == 'formdef'
153
    assert formdata.evolution[1].parts[0].formdef_id == str(formdef.id)
154
    assert formdata.evolution[1].parts[0].old_data == {'1': 'bar', 'bo1': 'foobar'}
155
    assert formdata.evolution[1].parts[0].new_data == {'1': 'baz', 'bo1': 'foobar'}
156
    # update, bo field first action
157
    assert formdata.evolution[2].parts[0].formdef_type == 'formdef'
158
    assert formdata.evolution[2].parts[0].formdef_id == str(formdef.id)
159
    assert formdata.evolution[2].parts[0].old_data == {'1': 'baz', 'bo1': 'foobar'}
160
    assert formdata.evolution[2].parts[0].new_data == {'1': 'baz', 'bo1': 'baz'}
161
    # update, bo field second action
162
    assert formdata.evolution[2].parts[1].formdef_type == 'formdef'
163
    assert formdata.evolution[2].parts[1].formdef_id == str(formdef.id)
164
    assert formdata.evolution[2].parts[1].old_data == {'1': 'baz', 'bo1': 'baz'}
165
    assert formdata.evolution[2].parts[1].new_data == {'1': 'baz', 'bo1': 'foobaz'}
166

  
167

  
168
def test_backoffice_formdata_submission(pub, user):
169
    FormDef.wipe()
170
    formdef = FormDef()
171
    formdef.name = 'form title'
172
    formdef.fields = [
173
        fields.StringField(id='1', label='String', type='string'),
174
    ]
175
    formdef.workflow_roles = {'_receiver': user.roles[0]}
176
    formdef.backoffice_submission_roles = user.roles[:]
177
    formdef.store()
178

  
179
    app = login(get_app(pub))
180
    resp = app.get('/backoffice/submission/form-title/')
181
    resp.form['f1'] = 'test submission'
182
    resp = resp.form.submit('submit')  # -> validation
183
    resp = resp.form.submit('submit').follow()  # -> submitted
184
    assert 'The form has been recorded' in resp.text
185

  
186
    formdata = formdef.data_class().select()[0]
187
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
188
    assert formdata.evolution[0].parts[0].formdef_type == 'formdef'
189
    assert formdata.evolution[0].parts[0].formdef_id == str(formdef.id)
190
    assert formdata.evolution[0].parts[0].old_data == {}
191
    assert formdata.evolution[0].parts[0].new_data == {'1': 'test submission'}
192

  
193

  
194
def test_backoffice_carddata_add_edit(pub, user):
195
    CardDef.wipe()
196
    carddef = CardDef()
197
    carddef.name = 'test'
198
    carddef.fields = [
199
        fields.StringField(id='1', label='string', varname='foo'),
200
    ]
201
    carddef.backoffice_submission_roles = user.roles
202
    carddef.workflow_roles = {
203
        '_viewer': user.roles[0],
204
        '_editor': user.roles[0],
205
    }
206
    carddef.store()
207
    carddef.data_class().wipe()
208

  
209
    Workflow.wipe()
210
    workflow = Workflow(name='test')
211
    workflow.roles = {
212
        '_viewer': 'Viewer',
213
        '_editor': 'Editor',
214
    }
215
    st1 = workflow.add_status('Status1', 'st1')
216
    jump = st1.add_action('jump')
217
    jump.status = 'st2'
218
    st2 = workflow.add_status('Status2', 'st2')
219
    editable = st2.add_action('editable', id='_editable')
220
    editable.by = ['_editor']
221
    editable.status = st1.id
222
    workflow.store()
223

  
224
    carddef.workflow_id = workflow.id
225
    carddef.store()
226
    carddef.data_class().wipe()
227

  
228
    app = login(get_app(pub))
229
    resp = app.get('/backoffice/data/test/add/')
230
    resp.form['f1'] = 'foo'
231
    resp = resp.form.submit('submit').follow()
232
    assert 'button_editable-button' in resp.text
233

  
234
    resp = resp.form.submit('button_editable')
235
    resp = resp.follow()
236
    resp.form['f1'].value = 'bar'
237
    resp = resp.form.submit('submit').follow()
238

  
239
    carddata = carddef.data_class().select()[0]
240
    # creation
241
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
242
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
243
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
244
    assert carddata.evolution[0].parts[0].old_data == {}
245
    assert carddata.evolution[0].parts[0].new_data == {'1': 'foo'}
246
    # update
247
    assert isinstance(carddata.evolution[1].parts[0], ContentSnapshotPart)
248
    assert carddata.evolution[1].parts[0].formdef_type == 'carddef'
249
    assert carddata.evolution[1].parts[0].formdef_id == str(carddef.id)
250
    assert carddata.evolution[1].parts[0].old_data == {'1': 'foo'}
251
    assert carddata.evolution[1].parts[0].new_data == {'1': 'bar'}
252

  
253

  
254
def test_backoffice_card_import_csv(pub, user):
255
    CardDef.wipe()
256
    carddef = CardDef()
257
    carddef.name = 'test'
258
    carddef.fields = [
259
        fields.StringField(id='1', label='Test'),
260
        fields.StringField(id='2', label='Test2'),
261
    ]
262
    carddef.workflow_roles = {'_editor': user.roles[0]}
263
    carddef.backoffice_submission_roles = user.roles
264
    carddef.store()
265
    carddef.data_class().wipe()
266

  
267
    app = login(get_app(pub))
268

  
269
    resp = app.get(carddef.get_url())
270
    resp = resp.click('Import data from a file')
271
    data = [b'Test,Test2']
272
    data.append(b'data,foo')
273

  
274
    resp.forms[0]['file'] = Upload('test.csv', b'\n'.join(data), 'text/csv')
275
    resp = resp.forms[0].submit().follow()
276
    assert 'Importing data into cards' in resp
277
    assert carddef.data_class().count() == 1
278
    carddata = carddef.data_class().select()[0]
279
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
280
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
281
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
282
    assert carddata.evolution[-1].parts[0].old_data == {}
283
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'data', '2': 'foo'}
284

  
285

  
286
def test_backoffice_card_import_json(pub, user):
287
    CardDef.wipe()
288
    carddef = CardDef()
289
    carddef.name = 'test'
290
    carddef.fields = [
291
        fields.StringField(id='1', label='Test', varname='string'),
292
    ]
293
    carddef.workflow_roles = {'_editor': user.roles[0]}
294
    carddef.backoffice_submission_roles = user.roles
295
    carddef.store()
296
    carddef.data_class().wipe()
297

  
298
    app = login(get_app(pub))
299
    resp = app.get(carddef.get_url())
300
    resp = resp.click('Import data from a file')
301
    data = {
302
        'data': [
303
            {
304
                'fields': {
305
                    'string': 'a string',
306
                }
307
            }
308
        ]
309
    }
310
    resp.forms[0]['file'] = Upload('test.json', json.dumps(data).encode(), 'application/json')
311
    resp = resp.forms[0].submit()
312
    assert '/backoffice/processing?job=' in resp.location
313
    resp = resp.follow()
314

  
315
    carddata = carddef.data_class().select()[0]
316
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
317
    assert carddata.evolution[-1].parts[0].formdef_type == 'carddef'
318
    assert carddata.evolution[-1].parts[0].formdef_id == str(carddef.id)
319
    assert carddata.evolution[-1].parts[0].old_data == {}
320
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'a string'}
321

  
322

  
323
def test_workflow_formdata_create(pub):
324
    FormDef.wipe()
325
    pub.tracking_code_class.wipe()
326

  
327
    target_formdef = FormDef()
328
    target_formdef.name = 'target form'
329
    target_formdef.fields = [
330
        fields.StringField(id='0', label='string', varname='string'),
331
    ]
332
    target_formdef.store()
333

  
334
    wf = Workflow(name='create-formdata')
335
    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
336
    create = wf.possible_status[1].add_action('create_formdata', id='_create', prepend=True)
337
    create.formdef_slug = target_formdef.url_name
338
    create.label = 'create a new linked form'
339
    create.varname = 'resubmitted'
340
    create.mappings = [
341
        Mapping(field_id='0', expression='{{ form_var_foo }}'),
342
    ]
343
    wf.store()
344

  
345
    source_formdef = FormDef()
346
    source_formdef.name = 'source form'
347
    source_formdef.fields = [
348
        fields.StringField(id='0', label='string', varname='foo'),
349
    ]
350
    source_formdef.workflow_id = wf.id
351
    source_formdef.store()
352

  
353
    formdata = source_formdef.data_class()()
354
    formdata.data = {'0': 'foobar'}
355
    formdata.just_created()
356
    formdata.store()
357

  
358
    formdata.perform_workflow()
359
    assert target_formdef.data_class().count() == 1
360
    created_formdata = target_formdef.data_class().select()[0]
361
    assert isinstance(created_formdata.evolution[0].parts[0], ContentSnapshotPart)
362
    assert created_formdata.evolution[0].parts[0].formdef_type == 'formdef'
363
    assert created_formdata.evolution[0].parts[0].formdef_id == str(target_formdef.id)
364
    assert created_formdata.evolution[0].parts[0].old_data == {}
365
    assert created_formdata.evolution[0].parts[0].new_data == {'0': 'foobar'}
366

  
367

  
368
def test_workflow_carddata_create(pub):
369
    CardDef.wipe()
370
    FormDef.wipe()
371

  
372
    carddef = CardDef()
373
    carddef.name = 'My card'
374
    carddef.fields = [
375
        fields.StringField(id='1', label='string'),
376
    ]
377
    carddef.store()
378

  
379
    wf = Workflow(name='create-carddata')
380
    wf.possible_status = Workflow.get_default_workflow().possible_status[:]
381
    create = wf.possible_status[1].add_action('create_carddata', id='_create', prepend=True)
382
    create.label = 'Create CardDef'
383
    create.varname = 'mycard'
384
    create.formdef_slug = carddef.url_name
385
    create.mappings = [
386
        Mapping(field_id='1', expression='{{ form_var_string }}'),
387
    ]
388
    wf.store()
389

  
390
    formdef = FormDef()
391
    formdef.name = 'source form'
392
    formdef.fields = [
393
        fields.StringField(id='1', label='string', varname='string'),
394
    ]
395
    formdef.workflow_id = wf.id
396
    formdef.store()
397

  
398
    formdata = formdef.data_class()()
399
    formdata.data = {'1': 'foobar'}
400
    formdata.just_created()
401
    formdata.store()
402

  
403
    formdata.perform_workflow()
404
    assert carddef.data_class().count() == 1
405
    carddata = carddef.data_class().select()[0]
406
    assert isinstance(carddata.evolution[-1].parts[0], ContentSnapshotPart)
407
    assert carddata.evolution[-1].parts[0].formdef_type == 'carddef'
408
    assert carddata.evolution[-1].parts[0].formdef_id == str(carddef.id)
409
    assert carddata.evolution[-1].parts[0].old_data == {}
410
    assert carddata.evolution[-1].parts[0].new_data == {'1': 'foobar'}
411

  
412

  
413
def test_workflow_carddata_edit(pub):
414
    FormDef.wipe()
415
    CardDef.wipe()
416

  
417
    # carddef
418
    carddef = CardDef()
419
    carddef.name = 'Model 1'
420
    carddef.fields = [
421
        fields.StringField(id='1', label='string', varname='string'),
422
    ]
423
    carddef.store()
424
    carddef.data_class().wipe()
425

  
426
    carddata = carddef.data_class()()
427
    carddata.data = {
428
        '1': 'foobar',
429
    }
430
    carddata.just_created()
431
    carddata.store()
432

  
433
    # formdef workflow that will update carddata
434
    wf = Workflow(name='update')
435
    st1 = wf.add_status('New', 'st1')
436
    jump = st1.add_action('jump', id='_jump')
437
    jump.by = ['_submitter', '_receiver']
438
    jump.status = 'st2'
439
    st2 = wf.add_status('Update card', 'st2')
440
    edit = st2.add_action('edit_carddata', id='edit')
441
    edit.formdef_slug = carddef.url_name
442
    edit.target_mode = 'manual'
443
    edit.target_id = '{{ form_var_card }}'
444
    edit.mappings = [
445
        Mapping(field_id='1', expression='{{ form_var_foo }}'),
446
    ]
447
    wf.store()
448

  
449
    # associated formdef
450
    formdef = FormDef()
451
    formdef.name = 'Update'
452
    formdef.fields = [
453
        fields.StringField(id='1', label='string', varname='card'),
454
        fields.StringField(id='2', label='string', varname='foo'),
455
    ]
456
    formdef.workflow = wf
457
    formdef.store()
458

  
459
    formdata = formdef.data_class()()
460
    formdata.data = {
461
        '1': str(carddata.id),
462
        '2': 'foobaz',
463
    }
464
    formdata.just_created()
465
    formdata.store()
466
    formdata.perform_workflow()
467
    carddata.refresh_from_storage()
468
    # creation
469
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
470
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
471
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
472
    assert carddata.evolution[0].parts[0].old_data == {}
473
    assert carddata.evolution[0].parts[0].new_data == {'1': 'foobar'}
474
    # update
475
    assert isinstance(carddata.evolution[1].parts[0], ContentSnapshotPart)
476
    assert carddata.evolution[1].parts[0].formdef_type == 'carddef'
477
    assert carddata.evolution[1].parts[0].formdef_id == str(carddef.id)
478
    assert carddata.evolution[1].parts[0].old_data == {'1': 'foobar'}
479
    assert carddata.evolution[1].parts[0].new_data == {'1': 'foobaz'}
480

  
481

  
482
def test_workflow_set_backoffice_field(http_requests, pub):
483
    Workflow.wipe()
484
    FormDef.wipe()
485
    wf = Workflow(name='xxx')
486
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
487
    wf.backoffice_fields_formdef.fields = [
488
        fields.StringField(id='bo1', label='1st backoffice field', type='string', varname='backoffice_blah'),
489
    ]
490
    st1 = wf.add_status('Status1')
491
    wf.store()
492

  
493
    formdef = FormDef()
494
    formdef.name = 'baz'
495
    formdef.fields = [
496
        fields.StringField(id='0', label='String', type='string', varname='string'),
497
    ]
498
    formdef.workflow_id = wf.id
499
    formdef.store()
500

  
501
    formdata = formdef.data_class()()
502
    formdata.data = {'0': 'HELLO'}
503
    formdata.just_created()
504
    formdata.store()
505
    pub.substitutions.feed(formdata)
506

  
507
    item = SetBackofficeFieldsWorkflowStatusItem()
508
    item.fields = [{'field_id': 'bo1', 'value': '{{ form_var_string }}'}]
509
    item.parent = st1
510

  
511
    item.perform(formdata)
512
    formdata.refresh_from_storage()
513
    # creation
514
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
515
    assert formdata.evolution[-1].parts[0].formdef_type == 'formdef'
516
    assert formdata.evolution[-1].parts[0].formdef_id == str(formdef.id)
517
    assert formdata.evolution[-1].parts[0].old_data == {}
518
    assert formdata.evolution[-1].parts[0].new_data == {
519
        '0': 'HELLO',
520
    }
521
    # bo action
522
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
523
    assert formdata.evolution[-1].parts[1].formdef_type == 'formdef'
524
    assert formdata.evolution[-1].parts[1].formdef_id == str(formdef.id)
525
    assert formdata.evolution[-1].parts[1].old_data == {
526
        '0': 'HELLO',
527
    }
528
    assert formdata.evolution[-1].parts[1].new_data == {
529
        '0': 'HELLO',
530
        'bo1': 'HELLO',
531
    }
532

  
533

  
534
def test_api_form_submit(pub, user, access, role):
535
    app = get_app(pub)
536
    app.set_authorization(('Basic', ('test', '12345')))
537

  
538
    FormDef.wipe()
539
    formdef = FormDef()
540
    formdef.name = 'test'
541
    formdef.fields = [
542
        fields.StringField(id='1', label='foobar', varname='foobar'),
543
    ]
544
    formdef.backoffice_submission_roles = [role.id]
545
    formdef.store()
546
    data_class = formdef.data_class()
547

  
548
    payload = {
549
        'data': {
550
            'foobar': 'xxx',
551
        }
552
    }
553
    resp = app.post_json(
554
        '/api/formdefs/test/submit',
555
        payload,
556
    )
557
    assert resp.json['err'] == 0
558
    formdata = data_class.get(resp.json['data']['id'])
559
    assert isinstance(formdata.evolution[0].parts[0], ContentSnapshotPart)
560
    assert formdata.evolution[0].parts[0].formdef_type == 'formdef'
561
    assert formdata.evolution[0].parts[0].formdef_id == str(formdef.id)
562
    assert formdata.evolution[0].parts[0].old_data == {}
563
    assert formdata.evolution[0].parts[0].new_data == {'1': 'xxx'}
564

  
565

  
566
def test_api_formdata_edit(pub, user, access, role):
567
    app = get_app(pub)
568
    app.set_authorization(('Basic', ('test', '12345')))
569

  
570
    FormDef.wipe()
571
    formdef = FormDef()
572
    formdef.name = 'test'
573
    formdef.fields = [
574
        fields.StringField(id='0', label='foobar', varname='foobar'),
575
    ]
576
    Workflow.wipe()
577
    workflow = Workflow(name='foo')
578
    workflow.possible_status = Workflow.get_default_workflow().possible_status[:]
579
    workflow.store()
580
    formdef.workflow_id = workflow.id
581
    formdef.workflow_roles = {'_receiver': role.id}
582
    formdef.store()
583
    formdef.data_class().wipe()
584
    formdata = formdef.data_class()()
585
    formdata.data = {
586
        '0': 'foo@localhost',
587
    }
588
    formdata.user_id = user.id
589
    formdata.just_created()
590
    formdata.status = 'wf-new'
591
    formdata.evolution[-1].status = 'wf-new'
592
    formdata.store()
593

  
594
    wfedit = workflow.possible_status[1].add_action('editable', id='_wfedit')
595
    wfedit.by = [user.roles[0]]
596
    workflow.store()
597

  
598
    app.post_json(
599
        '/api/forms/test/%s/' % formdata.id,
600
        {'data': {'0': 'bar@localhost'}},
601
    )
602
    formdata.refresh_from_storage()
603
    # creation
604
    assert isinstance(formdata.evolution[-1].parts[0], ContentSnapshotPart)
605
    assert formdata.evolution[-1].parts[0].formdef_type == 'formdef'
606
    assert formdata.evolution[-1].parts[0].formdef_id == str(formdef.id)
607
    assert formdata.evolution[-1].parts[0].old_data == {}
608
    assert formdata.evolution[-1].parts[0].new_data == {'0': 'foo@localhost'}
609
    # update
610
    assert isinstance(formdata.evolution[-1].parts[1], ContentSnapshotPart)
611
    assert formdata.evolution[-1].parts[1].formdef_type == 'formdef'
612
    assert formdata.evolution[-1].parts[1].formdef_id == str(formdef.id)
613
    assert formdata.evolution[-1].parts[1].old_data == {'0': 'foo@localhost'}
614
    assert formdata.evolution[-1].parts[1].new_data == {'0': 'bar@localhost'}
615

  
616

  
617
def test_api_card_import_csv(pub, user, access, role):
618
    app = get_app(pub)
619
    app.set_authorization(('Basic', ('test', '12345')))
620

  
621
    CardDef.wipe()
622
    carddef = CardDef()
623
    carddef.name = 'test'
624
    carddef.fields = [
625
        fields.StringField(id='0', label='foobar', varname='foo'),
626
        fields.StringField(id='1', label='foobar2', varname='foo2'),
627
    ]
628
    carddef.workflow_roles = {'_viewer': role.id}
629
    carddef.backoffice_submission_roles = [role.id]
630
    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
631
    carddef.store()
632

  
633
    carddef.data_class().wipe()
634

  
635
    resp = app.put(
636
        '/api/cards/test/import-csv',
637
        params=b'foobar;foobar2\nfirst entry;plop\n',
638
        headers={'content-type': 'text/csv'},
639
    )
640
    assert resp.json == {'err': 0}
641
    assert carddef.data_class().count() == 1
642
    carddata = carddef.data_class().select()[0]
643
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
644
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
645
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
646
    assert carddata.evolution[0].parts[0].old_data == {}
647
    assert carddata.evolution[0].parts[0].new_data == {'0': 'first entry', '1': 'plop'}
648

  
649

  
650
def test_api_card_import_json(pub, user, access, role):
651
    app = get_app(pub)
652
    app.set_authorization(('Basic', ('test', '12345')))
653

  
654
    CardDef.wipe()
655
    carddef = CardDef()
656
    carddef.name = 'test'
657
    carddef.fields = [
658
        fields.StringField(id='0', label='foobar', varname='foo'),
659
    ]
660
    carddef.workflow_roles = {'_viewer': role.id}
661
    carddef.backoffice_submission_roles = [role.id]
662
    carddef.store()
663

  
664
    carddef.data_class().wipe()
665

  
666
    data = {
667
        'data': [
668
            {
669
                'fields': {
670
                    'foo': 'bar',
671
                }
672
            },
673
        ]
674
    }
675
    resp = app.put_json(
676
        '/api/cards/test/import-json',
677
        data,
678
    )
679
    assert resp.json == {'err': 0}
680
    assert carddef.data_class().count() == 1
681
    carddata = carddef.data_class().select()[0]
682
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
683
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
684
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
685
    assert carddata.evolution[0].parts[0].old_data == {}
686
    assert carddata.evolution[0].parts[0].new_data == {'0': 'bar'}
687

  
688

  
689
def test_api_card_submit(pub, user, access, role):
690
    app = get_app(pub)
691
    app.set_authorization(('Basic', ('test', '12345')))
692

  
693
    CardDef.wipe()
694
    carddef = CardDef()
695
    carddef.name = 'test'
696
    carddef.fields = [
697
        fields.StringField(id='1', label='foobar', varname='foobar'),
698
    ]
699
    carddef.backoffice_submission_roles = [role.id]
700
    carddef.store()
701
    data_class = carddef.data_class()
702

  
703
    payload = {
704
        'data': {
705
            'foobar': 'xxx',
706
        }
707
    }
708
    resp = app.post_json(
709
        '/api/cards/test/submit',
710
        payload,
711
    )
712
    assert resp.json['err'] == 0
713
    carddata = data_class.get(resp.json['data']['id'])
714
    assert isinstance(carddata.evolution[0].parts[0], ContentSnapshotPart)
715
    assert carddata.evolution[0].parts[0].formdef_type == 'carddef'
716
    assert carddata.evolution[0].parts[0].formdef_id == str(carddef.id)
717
    assert carddata.evolution[0].parts[0].old_data == {}
718
    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
......
452 453
        return real_action
453 454

  
454 455

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

  
463

  
455 464
class DuplicateGlobalActionNameError(Exception):
456 465
    pass
457 466

  
458
-