Projet

Général

Profil

0001-workflows-store-workflow-form-data-for-structured-ac.patch

Frédéric Péters, 30 novembre 2021 13:20

Télécharger (15,7 ko)

Voir les différences:

Subject: [PATCH] workflows: store workflow form data for structured access
 (#50795)

 tests/form_pages/test_all.py |  96 ++++++++++++++++++++++++++++++
 tests/test_formdef.py        |  35 +++++++++--
 wcs/formdef.py               |   5 ++
 wcs/variables.py             |   9 +++
 wcs/wf/form.py               | 109 ++++++++++++++++++++++++++++++++---
 5 files changed, 242 insertions(+), 12 deletions(-)
tests/form_pages/test_all.py
14 14
from webtest import Hidden, Radio, Upload
15 15

  
16 16
from wcs import fields
17
from wcs.blocks import BlockDef
17 18
from wcs.carddef import CardDef
18 19
from wcs.categories import Category
19 20
from wcs.data_sources import NamedDataSource
......
23 24
from wcs.qommon.emails import docutils
24 25
from wcs.qommon.ident.password_accounts import PasswordAccount
25 26
from wcs.qommon.misc import ConnectionError
27
from wcs.qommon.substitution import CompatibilityNamesDict
26 28
from wcs.roles import logged_users_role
27 29
from wcs.tracking_code import TrackingCode
28 30
from wcs.wf.backoffice_fields import SetBackofficeFieldsWorkflowStatusItem
......
7113 7115

  
7114 7116
    formdata = formdef.data_class()()
7115 7117
    formdata.user_id = user.id
7118
    formdata.just_created()
7116 7119
    formdata.status = 'wf-new'
7117 7120
    formdata.data = {'0': 'plop'}
7118 7121
    formdata.store()
......
7253 7256

  
7254 7257
    formdata = formdef.data_class()()
7255 7258
    formdata.user_id = user.id
7259
    formdata.just_created()
7256 7260
    formdata.status = 'wf-new'
7257 7261
    formdata.data = {'0': 'plop'}
7258 7262
    formdata.store()
......
8169 8173
    # and persist after being saved again
8170 8174
    resp = resp.form.submit('submit').follow()
8171 8175
    assert '<span>test.txt</span>' in resp.text
8176

  
8177

  
8178
def test_workflow_form_structured_data(pub):
8179
    FormDef.wipe()
8180
    Workflow.wipe()
8181
    BlockDef.wipe()
8182

  
8183
    user = create_user(pub)
8184

  
8185
    block = BlockDef()
8186
    block.name = 'foobar'
8187
    block.fields = [
8188
        fields.StringField(id='123', required=True, label='Test', type='string', varname='test'),
8189
    ]
8190
    block.store()
8191

  
8192
    wf = Workflow(name='test')
8193
    status = wf.add_status('New', 'st1')
8194
    status.items = []
8195
    display_form = FormWorkflowStatusItem()
8196
    display_form.id = '_display_form'
8197
    display_form.by = ['_submitter']
8198
    display_form.varname = 'blah'
8199
    display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
8200
    display_form.formdef.fields = [
8201
        fields.BlockField(id='1', label='test', type='block:foobar', varname='fooblock'),
8202
    ]
8203
    status.items.append(display_form)
8204
    display_form.parent = status
8205
    wf.store()
8206

  
8207
    formdef = create_formdef()
8208
    formdef.workflow_id = wf.id
8209
    formdef.fields = []
8210
    formdef.store()
8211

  
8212
    formdef.data_class().wipe()
8213

  
8214
    formdata = formdef.data_class()()
8215
    formdata.user_id = user.id
8216
    formdata.just_created()
8217
    formdata.store()
8218

  
8219
    app = login(get_app(pub), username='foo', password='foo')
8220
    resp = app.get(formdata.get_url(backoffice=False))
8221
    resp.form['fblah_1$element0$f123'] = 'ABC'
8222
    resp = resp.form.submit('submit').follow()
8223

  
8224
    resp.form['fblah_1$element0$f123'] = 'XYZ'
8225
    resp = resp.form.submit('submit').follow()
8226

  
8227
    formdata.refresh_from_storage()
8228
    assert formdata.workflow_data == {
8229
        'blah_var_fooblock_raw': {'data': [{'123': 'XYZ'}], 'schema': {'123': 'string'}},
8230
        'blah_var_fooblock': 'foobar',
8231
    }
8232

  
8233
    substvars = CompatibilityNamesDict()
8234
    substvars.update(formdata.get_substitution_variables())
8235
    keys = substvars.get_flat_keys()
8236
    for key in keys:
8237
        # noqa pylint: disable=unused-variable
8238
        var = substvars[key]  # check it doesn't raise, ignore the value
8239

  
8240
    assert substvars['form_workflow_form_blah_var_fooblock_var_test'] == 'ABC'
8241
    assert substvars['form_workflow_form_blah_0_var_fooblock_var_test'] == 'ABC'
8242
    assert substvars['form_workflow_form_blah_1_var_fooblock_var_test'] == 'XYZ'
8243
    assert substvars['form_workflow_form_blah_latest_var_fooblock_var_test'] == 'XYZ'
8244

  
8245
    # disable dumping in workflow_data
8246
    pub.load_site_options()
8247
    if not pub.site_options.has_section('options'):
8248
        pub.site_options.add_section('options')
8249
    pub.site_options.set('options', 'disable-workflow-form-to-workflow-data', 'true')
8250
    with open(os.path.join(pub.app_dir, 'site-options.cfg'), 'w') as fd:
8251
        pub.site_options.write(fd)
8252

  
8253
    formdata = formdef.data_class()()
8254
    formdata.user_id = user.id
8255
    formdata.just_created()
8256
    formdata.store()
8257

  
8258
    app = login(get_app(pub), username='foo', password='foo')
8259
    resp = app.get(formdata.get_url(backoffice=False))
8260
    resp.form['fblah_1$element0$f123'] = 'ABC'
8261
    resp = resp.form.submit('submit').follow()
8262

  
8263
    resp.form['fblah_1$element0$f123'] = 'XYZ'
8264
    resp = resp.form.submit('submit').follow()
8265

  
8266
    formdata.refresh_from_storage()
8267
    assert not formdata.workflow_data
tests/test_formdef.py
17 17
from wcs.formdef import FormDef, get_formdefs_of_all_kinds
18 18
from wcs.qommon.http_request import HTTPRequest
19 19
from wcs.qommon.upload_storage import PicklableUpload
20
from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormFieldsFormDef
20
from wcs.wf.form import FormWorkflowStatusItem, WorkflowFormEvolutionPart, WorkflowFormFieldsFormDef
21 21
from wcs.workflows import (
22 22
    AttachmentEvolutionPart,
23 23
    Workflow,
......
460 460
        assert len(glob.glob(os.path.join(pub.app_dir, 'attachments', '*/*'))) == 0
461 461

  
462 462
        # files in user profile
463

  
464 463
        user_formdef = UserFieldsFormDef(pub)
465 464
        user_formdef.fields.append(fields.FileField(id='3', label='test', type='file'))
466 465
        user_formdef.store()
......
479 478
        clean_unused_files(pub)
480 479
        assert len(os.listdir(os.path.join(pub.app_dir, 'uploads'))) == 0
481 480

  
481
        # file from workflow form
482
        formdata = formdef.data_class()()
483
        formdata.just_created()
484
        formdata.data = {}
485
        formdata.store()
486

  
487
        display_form = FormWorkflowStatusItem()
488
        display_form.varname = 'blah'
489
        display_form.formdef = WorkflowFormFieldsFormDef(item=display_form)
490
        display_form.formdef.fields = []
491

  
492
        data = {'blah_1': PicklableUpload('test.txt', 'text/plain')}
493
        data['blah_1'].receive([b'hello world wf form'])
494
        formdata.evolution[-1].parts = [
495
            WorkflowFormEvolutionPart(display_form, data),
496
        ]
497

  
498
        count = len(os.listdir(os.path.join(pub.app_dir, 'uploads')))
499
        formdata.store()
500
        assert len(os.listdir(os.path.join(pub.app_dir, 'uploads'))) == count + 1
501
        clean_unused_files(pub)
502
        assert len(os.listdir(os.path.join(pub.app_dir, 'uploads'))) == count + 1
503

  
504
        formdata.evolution[-1].parts = []
505
        formdata.store()
506
        clean_unused_files(pub)
507
        assert len(os.listdir(os.path.join(pub.app_dir, 'uploads'))) == count
508

  
482 509
        if behaviour == 'move':
483
            # 3 files ("hello world" + "hello world 2" + "hello world block")
484
            assert len(os.listdir(os.path.join(pub.app_dir, 'unused-files/uploads/'))) == 3
510
            # 4 files ("hello world" + "hello world 2" + "hello world block" + "hello world wf form")
511
            assert len(os.listdir(os.path.join(pub.app_dir, 'unused-files/uploads/'))) == 4
485 512
            # 1 attachment
486 513
            assert len(glob.glob(os.path.join(pub.app_dir, 'unused-files/attachments/*/*'))) == 1
487 514

  
wcs/formdef.py
1893 1893

  
1894 1894
    def accumulate_filenames():
1895 1895
        from wcs.carddef import CardDef
1896
        from wcs.wf.form import WorkflowFormEvolutionPart
1896 1897

  
1897 1898
        for formdef in FormDef.select(ignore_migration=True) + CardDef.select(ignore_migration=True):
1898 1899
            for option_data in (formdef.workflow_options or {}).values():
......
1913 1914
                for part in formdata.iter_evolution_parts():
1914 1915
                    if is_attachment(part):
1915 1916
                        yield part.filename
1917
                    elif isinstance(part, WorkflowFormEvolutionPart):
1918
                        for field_data in (part.data or {}).values():
1919
                            if is_upload(field_data):
1920
                                yield field_data.get_fs_filename()
1916 1921
        for user in publisher.user_class.select():
1917 1922
            for field_data in (user.form_data or {}).values():
1918 1923
                if is_upload(field_data):
wcs/variables.py
596 596

  
597 597
        return LazyFormDataLinks(self._formdata)
598 598

  
599
    @property
600
    def workflow_form(self):
601
        # form_ workflow_form_ <slug (action varname)> _ <index> _var_ etc.
602
        # ex: form_workflow_form_xxx_2_var_file
603
        # (index can be "latest")
604
        from .wf.form import LazyFormDataWorkflowForms
605

  
606
        return LazyFormDataWorkflowForms(self._formdata)
607

  
599 608
    @property
600 609
    def parent(self):
601 610
        formdata = self._formdata.get_parent()
wcs/wf/form.py
24 24
from wcs.formdef import FormDef, lax_int
25 25
from wcs.forms.common import FileDirectory
26 26
from wcs.forms.root import FormPage
27
from wcs.workflows import RedisplayFormException, WorkflowStatusItem, register_item_class
27
from wcs.variables import LazyFormDataVar
28
from wcs.workflows import EvolutionPart, RedisplayFormException, WorkflowStatusItem, register_item_class
28 29

  
29 30
from ..qommon import _
30 31
from ..qommon.form import SingleSelectWidget, VarnameWidget, WidgetList
31 32

  
32 33

  
34
class WorkflowFormEvolutionPart(EvolutionPart):
35
    data = None
36
    formdef = None
37
    varname = None
38

  
39
    def __init__(self, action, data, live=False):
40
        self.varname = action.varname
41
        self.formdef = action.formdef
42
        self.data = data
43
        self.live = live
44

  
45

  
33 46
def lookup_wf_form_file(self, filename):
34 47
    # supports for URLs such as /$formdata/$id/files/form-$formvar-$fieldvar/test.txt
35 48
    try:
......
189 202
                self.formdef.readonly = True
190 203
            fields_directory = WorkflowFormFieldsDirectory(self.formdef)
191 204
            if self.varname:
192
                fields_directory.field_var_prefix = '%s_var_' % self.varname
205
                fields_directory.field_var_prefix = 'form_workflow_form_%s_var_' % self.varname
193 206
            fields_directory.html_top = html_top
194 207
            return fields_directory
195 208
        return None
......
229 242

  
230 243
        FormPage.apply_field_prefills({}, form, fields)
231 244

  
232
    def evaluate_live_form(self, form, formdata, user):
245
    def evaluate_live_form(self, form, formdata, user, submit=False):
233 246
        if not self.formdef:
234 247
            return
235 248
        workflow_data = {}
236 249
        self.prefix_form_fields()
237
        for k, v in get_dict_with_varnames(
238
            self.formdef.fields, self.formdef.get_data(form), varnames_only=True
239
        ).items():
250
        formdef_data = self.formdef.get_data(form)
251
        for k, v in get_dict_with_varnames(self.formdef.fields, formdef_data, varnames_only=True).items():
240 252
            workflow_data['%s_%s' % (self.varname, k)] = v
241
        formdata.update_workflow_data(workflow_data)
253
        if not get_publisher().has_site_option('disable-workflow-form-to-workflow-data'):
254
            formdata.update_workflow_data(workflow_data)
255
        if self.varname and submit:
256
            # do not keep parts used for live evaluation (evaluate_live_form will
257
            # be called when creating form and it shouldn't record twice the values)
258
            formdata.evolution[-1].parts = [
259
                x
260
                for x in formdata.evolution[-1].parts or []
261
                if not (isinstance(x, WorkflowFormEvolutionPart) and x.live)
262
            ]
263
            formdata.evolution[-1].add_part(
264
                WorkflowFormEvolutionPart(self, formdef_data, live=bool(not submit))
265
            )
242 266

  
243 267
    def submit_form(self, form, formdata, user, evo):
244 268
        if not self.formdef:
......
251 275
                    if add_element_widget and add_element_widget.parse():
252 276
                        raise RedisplayFormException()
253 277
        if form.get_submit() == 'submit' and not form.has_errors():
254
            self.evaluate_live_form(form, formdata, user)
278
            self.evaluate_live_form(form, formdata, user, submit=True)
255 279
            formdata.store()
256 280
        get_publisher().substitutions.unfeed(lambda x: x.__class__.__name__ == 'ConditionVars')
257 281

  
......
275 299

  
276 300

  
277 301
register_item_class(FormWorkflowStatusItem)
302

  
303

  
304
class LazyFormDataWorkflowForms:
305
    def __init__(self, formdata):
306
        self._formdata = formdata
307

  
308
    def __getattr__(self, varname):
309
        wfform_formdatas = []
310
        for part in self._formdata.iter_evolution_parts():
311
            if not isinstance(part, WorkflowFormEvolutionPart):
312
                continue
313
            if part.varname == varname and part.data:
314
                wfform_formdatas.append(LazyFormDataWorkflowFormsItem(part))
315
        if wfform_formdatas:
316
            return LazyFormDataWorkflowFormsItems(wfform_formdatas)
317
        raise AttributeError(varname)
318

  
319
    def inspect_keys(self):
320
        for part in self._formdata.iter_evolution_parts():
321
            if isinstance(part, WorkflowFormEvolutionPart) and part.varname and part.data:
322
                yield part.varname
323

  
324

  
325
class LazyFormDataWorkflowFormsItems:
326
    def __init__(self, wfform_formdatas):
327
        self._wfform_formdatas = wfform_formdatas
328

  
329
    def inspect_keys(self):
330
        return [str(x) for x in range(len(self._wfform_formdatas))] + ['var', 'latest']
331

  
332
    @property
333
    def var(self):
334
        # alias when there's a single workflow form
335
        return self._wfform_formdatas[0].var
336

  
337
    @property
338
    def latest(self):
339
        # alias to latest form
340
        return {'var': self._wfform_formdatas[-1].var}
341

  
342
    def __getitem__(self, key):
343
        try:
344
            key = int(key)
345
        except ValueError:
346
            try:
347
                return getattr(self, key)
348
            except AttributeError:
349
                return self._wfform_formdatas[0][key]
350
        return self._wfform_formdatas[key]
351

  
352
    def __len__(self):
353
        return len(self._wfform_formdatas)
354

  
355
    def __iter__(self):
356
        yield from self._wfform_formdatas
357

  
358

  
359
class LazyFormDataWorkflowFormsItem:
360
    def __init__(self, part):
361
        self._part = part
362
        self.data = part.data
363

  
364
    def inspect_keys(self):
365
        return ['var']
366

  
367
    @property
368
    def var(self):
369
        # pass self as formdata, it will be used to access self.data in LazyFieldVarBlock
370
        return LazyFormDataVar(self._part.formdef.get_all_fields(), self._part.data, formdata=self)
278
-