Projet

Général

Profil

0001-general-allow-assigning-complex-types-from-rendered-.patch

Frédéric Péters, 04 décembre 2020 16:45

Télécharger (22,4 ko)

Voir les différences:

Subject: [PATCH] general: allow assigning complex types from rendered
 templates (#41847)

 tests/test_workflows.py     | 196 ++++++++++++++++++++++++++++++++++--
 wcs/fields.py               |  14 ++-
 wcs/publisher.py            |  35 +++++++
 wcs/qommon/template.py      |   6 +-
 wcs/variables.py            |  11 +-
 wcs/wf/backoffice_fields.py |  20 +++-
 wcs/wf/create_formdata.py   |  20 +++-
 wcs/workflows.py            |   3 +-
 8 files changed, 279 insertions(+), 26 deletions(-)
tests/test_workflows.py
23 23
from wcs.qommon.errors import ConnectionError
24 24
from quixote.http_request import Upload as QuixoteUpload
25 25
from wcs.qommon.http_request import HTTPRequest
26
from wcs.qommon.form import PicklableUpload
26 27
from wcs.qommon.form import *
27 28

  
29
from wcs.blocks import BlockDef
28 30
from wcs.formdef import FormDef
29 31
from wcs.carddef import CardDef
30 32
from wcs import sessions
31 33
from wcs.fields import (StringField, DateField, MapField, FileField, ItemField,
32 34
        ItemsField, CommentField, EmailField, PageField, TitleField,
33
        SubtitleField, TextField, BoolField, TableField)
35
        SubtitleField, TextField, BoolField, TableField, BlockField)
34 36
from wcs.formdata import Evolution
35 37
from wcs.logged_errors import LoggedError
36 38
from wcs.roles import Role
......
3880 3882
    wf.backoffice_fields_formdef.fields = [
3881 3883
        FileField(id='bo1', label='1st backoffice field',
3882 3884
            type='file', varname='backoffice_file'),
3885
        StringField(id='bo2', label='2nd backoffice field', type='string'),
3883 3886
    ]
3884 3887
    st1 = wf.add_status('Status1')
3885 3888
    wf.store()
......
3940 3943
    assert formdata.data['bo1'].content_type == 'application/vnd.oasis.opendocument.text'
3941 3944
    assert formdata.data['bo1'].get_content() == open(os.path.join(os.path.dirname(__file__), 'template.odt'), 'rb').read()
3942 3945

  
3943
    # check striping metadata
3946
    # check with template string
3947
    formdata = formdef.data_class()()
3948
    formdata.data = {'00': upload}
3949
    formdata.just_created()
3950
    formdata.store()
3951

  
3952
    two_pubs.substitutions.feed(formdata)
3953
    item.fields = [{'field_id': 'bo1', 'value': '{{form_var_file_raw}}'}]
3954
    item.perform(formdata)
3955

  
3956
    assert formdata.data['bo1'].base_filename == 'test.jpeg'
3957
    assert formdata.data['bo1'].content_type == 'image/jpeg'
3958
    assert formdata.data['bo1'].get_content() == open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read()
3959

  
3960
    # check with template string, without _raw
3961
    formdata = formdef.data_class()()
3962
    formdata.data = {'00': upload}
3963
    formdata.just_created()
3964
    formdata.store()
3965

  
3966
    two_pubs.substitutions.feed(formdata)
3967
    item.fields = [{'field_id': 'bo1', 'value': '{{form_var_file}}'}]
3968
    item.perform(formdata)
3969

  
3970
    assert formdata.data['bo1'].base_filename == 'test.jpeg'
3971
    assert formdata.data['bo1'].content_type == 'image/jpeg'
3972
    assert formdata.data['bo1'].get_content() == open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read()
3973

  
3974
    # check with a template string, into a string field
3975
    two_pubs.substitutions.feed(formdata)
3976
    item.fields = [{'field_id': 'bo2', 'value': '{{form_var_file}}'}]
3977
    item.perform(formdata)
3978

  
3979
    assert formdata.data['bo2'] == 'test.jpeg'
3980

  
3981
    # check with template string and missing file
3982
    formdata = formdef.data_class()()
3983
    formdata.data = {'00': None}
3984
    formdata.just_created()
3985
    formdata.store()
3986

  
3987
    assert formdata.data.get('bo1') is None
3988

  
3989
    # check stripping metadata
3944 3990
    two_pubs.substitutions.feed(formdata)
3945 3991
    item.fields = [{'field_id': 'bo1',
3946 3992
                    'value': '=utils.attachment(form_var_file_raw,' +
......
4028 4074

  
4029 4075
    hello_world = formdata.data['bo1']
4030 4076
    # check wrong value
4031
    for value in ('="HELLO"', 'BAD', '={}', '=[]'):
4077
    for value in ('="HELLO"', 'BAD'):
4032 4078
        formdata.data['bo1'] = hello_world
4033 4079
        formdata.store()
4034 4080

  
......
4339 4385
    assert {'id': 'a', 'more': 'aaa', 'text': 'aa'} in formdata.data['bo1_structured']
4340 4386
    assert {'id': 'c', 'more': 'ccc', 'text': 'cc'} in formdata.data['bo1_structured']
4341 4387

  
4388
    # from formdata field
4389
    formdef.fields = [
4390
            ItemsField(id='1', label='field', type='items', varname='items', data_source=datasource),
4391
    ]
4392
    formdef.store()
4393

  
4394
    formdata = formdef.data_class()()
4395
    formdata.data = {'1': ['a', 'c']}
4396
    formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, '1')
4397
    formdata.data['1_structured'] = formdef.fields[0].store_structured_value(formdata.data, '1')
4398
    formdata.just_created()
4399
    formdata.store()
4400
    two_pubs.substitutions.feed(formdata)
4401

  
4402
    item = SetBackofficeFieldsWorkflowStatusItem()
4403
    item.parent = st1
4404
    item.fields = [{'field_id': 'bo1', 'value': "=form_var_items_raw"}]
4405
    item.perform(formdata)
4406

  
4407
    assert formdata.data['bo1'] == ['a', 'c']
4408
    assert formdata.data['bo1_display'] == 'aa, cc'
4409
    assert len(formdata.data['bo1_structured']) == 2
4410
    assert {'id': 'a', 'more': 'aaa', 'text': 'aa'} in formdata.data['bo1_structured']
4411
    assert {'id': 'c', 'more': 'ccc', 'text': 'cc'} in formdata.data['bo1_structured']
4412

  
4413
    # with a template
4414
    formdata = formdef.data_class()()
4415
    formdata.data = {'1': ['a', 'c']}
4416
    formdata.data['1_display'] = formdef.fields[0].store_display_value(formdata.data, '1')
4417
    formdata.data['1_structured'] = formdef.fields[0].store_structured_value(formdata.data, '1')
4418
    formdata.just_created()
4419
    formdata.store()
4420
    two_pubs.substitutions.reset()
4421
    two_pubs.substitutions.feed(formdata)
4422

  
4423
    item.fields = [{'field_id': 'bo1', 'value': "{{form_var_items_raw}}"}]
4424
    item.perform(formdata)
4425

  
4342 4426

  
4343 4427
def test_set_backoffice_field_date(two_pubs):
4344 4428
    Workflow.wipe()
......
4460 4544
        formdata.store()
4461 4545

  
4462 4546

  
4547
def test_set_backoffice_field_block(two_pubs, blocks_feature):
4548
    BlockDef.wipe()
4549
    Workflow.wipe()
4550
    FormDef.wipe()
4551
    LoggedError.wipe()
4552

  
4553
    block = BlockDef()
4554
    block.name = 'foobar'
4555
    block.digest_template = 'X{{foobar_var_foo}}Y'
4556
    block.fields = [
4557
        StringField(id='123', required=True, label='Test',
4558
            type='string', varname='foo'),
4559
        StringField(id='234', required=True, label='Test2',
4560
            type='string', varname='bar'),
4561
    ]
4562
    block.store()
4563

  
4564
    wf = Workflow(name='xxx')
4565
    wf.backoffice_fields_formdef = WorkflowBackofficeFieldsFormDef(wf)
4566
    st1 = wf.add_status('Status1')
4567
    wf.backoffice_fields_formdef.fields = [
4568
        BlockField(id='bo1', label='1st backoffice field', type='block:foobar'),
4569
        StringField(id='bo2', label='2nd backoffice field', type='string'),
4570
    ]
4571
    wf.store()
4572

  
4573
    formdef = FormDef()
4574
    formdef.name = 'baz'
4575
    formdef.fields = [
4576
        BlockField(id='1', label='test', type='block:foobar', max_items=3, varname='foo'),
4577
    ]
4578
    formdef.workflow_id = wf.id
4579
    formdef.store()
4580

  
4581
    formdata = formdef.data_class()()
4582
    # value from test_block_digest in tests/test_form_pages.py
4583
    formdata.data = {
4584
        '1': {
4585
            'data': [{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}],
4586
            'schema': {'123': 'string', '234': 'string'}
4587
            },
4588
        '1_display': 'XfooY, Xfoo2Y',
4589
    }
4590
    formdata.just_created()
4591
    formdata.store()
4592
    get_publisher().substitutions.feed(formdata)
4593

  
4594
    item = SetBackofficeFieldsWorkflowStatusItem()
4595
    item.parent = st1
4596
    item.fields = [{'field_id': 'bo1', 'value': '{{form_var_foo_raw}}'}]
4597
    item.perform(formdata)
4598
    formdata = formdef.data_class().get(formdata.id)
4599
    assert formdata.data['bo1'] == formdata.data['1']
4600
    assert formdata.data['bo1_display'] == formdata.data['1_display']
4601

  
4602
    # without _raw suffix
4603
    formdata = formdef.data_class()()
4604
    # value from test_block_digest in tests/test_form_pages.py
4605
    formdata.data = {
4606
        '1': {
4607
            'data': [{'123': 'foo', '234': 'bar'}, {'123': 'foo2', '234': 'bar2'}],
4608
            'schema': {'123': 'string', '234': 'string'}
4609
            },
4610
        '1_display': 'XfooY, Xfoo2Y',
4611
    }
4612
    formdata.just_created()
4613
    formdata.store()
4614
    get_publisher().substitutions.reset()
4615
    get_publisher().substitutions.feed(formdata)
4616

  
4617
    item = SetBackofficeFieldsWorkflowStatusItem()
4618
    item.parent = st1
4619
    item.fields = [
4620
        {'field_id': 'bo1', 'value': '{{form_var_foo}}'},
4621
        {'field_id': 'bo2', 'value': '{{form_var_foo}}'},
4622
    ]
4623
    item.perform(formdata)
4624
    formdata = formdef.data_class().get(formdata.id)
4625
    assert formdata.data['bo1'] == formdata.data['1']
4626
    assert formdata.data['bo1_display'] == formdata.data['1_display']
4627
    assert formdata.data['bo2'] == formdata.data['1_display']
4628

  
4629

  
4463 4630
def test_set_backoffice_field_immediate_use(http_requests, two_pubs):
4464 4631
    Workflow.wipe()
4465 4632
    FormDef.wipe()
......
4902 5069
        StringField(id='1', label='string'),
4903 5070
        ItemField(id='2', label='List', items=['item1', 'item2'],
4904 5071
                  varname='clist'),
4905
        DateField(id='3', label='Date', varname='cdate')
5072
        DateField(id='3', label='Date', varname='cdate'),
5073
        FileField(id='4', label='File', varname='cfile'),
4906 5074
    ]
4907 5075
    carddef.store()
4908 5076

  
......
4917 5085
        Mapping(field_id='1', expression='=form_var_undefined'),
4918 5086
        Mapping(field_id='2', expression='{{ form_var_list }}'),
4919 5087
        Mapping(field_id='3', expression='{{ form_var_date }}'),
5088
        Mapping(field_id='4', expression='{{ form_var_file|default_if_none:"" }}'),
4920 5089
    ]
4921 5090
    create.parent = wf.possible_status[1]
4922 5091
    wf.possible_status[1].items.insert(0, create)
......
4925 5094
    formdef = FormDef()
4926 5095
    formdef.name = 'source form'
4927 5096
    formdef.fields = [
4928
        ItemField(id='1', label='List', items=['item1', 'item2'],
4929
                  varname='list'),
4930
        DateField(id='2', label='Date', varname='date')
5097
        ItemField(id='1', label='List', items=['item1', 'item2'], varname='list'),
5098
        DateField(id='2', label='Date', varname='date'),
5099
        FileField(id='3', label='File', varname='file'),
4931 5100
    ]
4932 5101
    formdef.workflow_id = wf.id
4933 5102
    formdef.store()
......
4954 5123
    formdata = formdef.data_class()()
4955 5124
    today = datetime.date.today()
4956 5125

  
5126
    upload = PicklableUpload('test.jpeg', 'image/jpeg')
5127
    with open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb') as jpg:
5128
        upload.receive([jpg.read()])
5129

  
4957 5130
    formdata.data = {'1': 'item1',
4958 5131
                     '1_display': 'item1',
4959
                     '2': today}
5132
                     '2': today,
5133
                     '3': upload}
4960 5134
    formdata.just_created()
4961 5135
    formdata.perform_workflow()
4962 5136

  
4963 5137
    assert formdata.get_substitution_variables()['form_links_mycard_form_number'] == '1-2'
4964 5138
    carddata = carddef.data_class().get(id=2)
4965
    assert carddata.get_substitution_variables()['form_var_clist'] == 'item1'
4966
    assert carddata.get_substitution_variables()['form_var_cdate'] == today
5139
    assert carddata.data['2'] == 'item1'
5140
    assert carddata.data['2_display'] == 'item1'
5141
    assert carddata.data['3'] == today.timetuple()
5142
    assert carddata.data['4'].base_filename == 'test.jpeg'
4967 5143

  
4968 5144
    create.condition = {'type': 'python', 'value': '1 == 2'}
4969 5145
    wf.store()
wcs/fields.py
176 176
    convert_value_from_str = None
177 177
    convert_value_to_str = None
178 178
    convert_value_from_anything = None
179
    allow_complex = False
179 180
    display_locations = []
180 181
    prefill = None
181 182
    store_display_value = None
......
967 968
class BoolField(WidgetField):
968 969
    key = 'bool'
969 970
    description = N_('Check Box (single choice)')
971
    allow_complex = True
970 972

  
971 973
    widget_class = CheckboxWidget
972 974
    required = False
......
1062 1064
class FileField(WidgetField):
1063 1065
    key = 'file'
1064 1066
    description = N_('File Upload')
1067
    allow_complex = True
1068

  
1065 1069
    document_type = None
1066 1070
    max_file_size = None
1067 1071
    automatic_image_resize = False
......
1118 1122

  
1119 1123
    @classmethod
1120 1124
    def convert_value_from_anything(cls, value):
1121
        if value is None:
1125
        if not value:
1122 1126
            return None
1123 1127
        from wcs.variables import LazyFieldVarFile
1124 1128
        if isinstance(value, LazyFieldVarFile):
......
1435 1439
class ItemField(WidgetField):
1436 1440
    key = 'item'
1437 1441
    description = N_('List')
1442
    allow_complex = True
1438 1443

  
1439 1444
    items = []
1440 1445
    show_as_radio = None
......
1728 1733
class ItemsField(WidgetField):
1729 1734
    key = 'items'
1730 1735
    description = N_('Multiple choice list')
1736
    allow_complex = True
1731 1737

  
1732 1738
    items = []
1733 1739
    max_choices = 0
......
2095 2101
class TableField(WidgetField):
2096 2102
    key = 'table'
2097 2103
    description = N_('Table')
2104
    allow_complex = True
2098 2105

  
2099 2106
    rows = None
2100 2107
    columns = None
......
2245 2252
class TableSelectField(TableField):
2246 2253
    key = 'table-select'
2247 2254
    description = N_('Table of Lists')
2255
    allow_complex = True
2248 2256

  
2249 2257
    items = None
2250 2258

  
......
2286 2294
class TableRowsField(WidgetField):
2287 2295
    key = 'tablerows'
2288 2296
    description = N_('Table with rows')
2297
    allow_complex = True
2289 2298

  
2290 2299
    total_row = True
2291 2300
    columns = None
......
2529 2538
class RankedItemsField(WidgetField):
2530 2539
    key = 'ranked-items'
2531 2540
    description = N_('Ranked Items')
2541
    allow_complex = True
2532 2542

  
2533 2543
    items = []
2534 2544
    randomize_items = False
......
2680 2690

  
2681 2691
class BlockField(WidgetField):
2682 2692
    key = 'block'
2693
    allow_complex = True
2694

  
2683 2695
    widget_class = BlockWidget
2684 2696
    max_items = 1
2685 2697
    extra_attributes = ['block', 'max_items', 'add_element_label', 'label_display']
wcs/publisher.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
from contextlib import contextmanager
17 18
import json
18 19
import os
19 20
import random
......
92 93
    tracking_code_class = TrackingCode
93 94
    unpickler_class = UnpicklerClass
94 95

  
96
    complex_data_cache = None
97

  
95 98
    @classmethod
96 99
    def get_backoffice_module(cls):
97 100
        import backoffice
......
375 378
            from . import sql
376 379
            sql.cleanup_connection()
377 380

  
381
    @contextmanager
382
    def complex_data(self):
383
        self.complex_data_cache = {}
384
        try:
385
            yield True
386
        finally:
387
            self.complex_data_cache = None
388

  
389
    def cache_complex_data(self, value, str_value=None):
390
        # Keep a temporary cache of assocations between a complex data value
391
        # (value) and a string reprensentation (str(value) or the dedicated
392
        # str_value parameter).
393
        #
394
        # It ensures string values are unique by appending a private unicode
395
        # code point, that will be removed in wcs/qommon/template.py.
396

  
397
        if self.complex_data_cache is None:
398
            # it doesn't do anything unless initialized.
399
            return str_value or value
400

  
401
        if str_value is None:
402
            str_value = str(value)
403
        str_value += chr(0xE000 + len(self.complex_data_cache))
404
        self.complex_data_cache[str_value] = value
405
        return str_value
406

  
407
    def get_cached_complex_data(self, value):
408
        if not isinstance(value, str):
409
            return value
410
        return (self.complex_data_cache or {}).get(value)
411

  
412

  
378 413
set_publisher_class(WcsPublisher)
379 414
WcsPublisher.register_extra_dir(os.path.join(os.path.dirname(__file__), 'extra'))
380 415

  
wcs/qommon/template.py
16 16

  
17 17
import os
18 18
import glob
19
import re
19 20
import xml.etree.ElementTree as ET
20 21

  
21 22
import django.template
......
505 506
                raise TemplateError(_('failure to render Django template: %s'), e)
506 507
            else:
507 508
                return self.value
508
        return force_str(rendered)
509
        rendered = str(rendered)
510
        if context.get('allow_complex'):
511
            return rendered
512
        return re.sub(r'[\uE000-\uF8FF]', '', rendered)
509 513

  
510 514
    def ezt_render(self, context={}):
511 515
        fd = StringIO()
wcs/variables.py
604 604
    @property
605 605
    def raw(self):
606 606
        if self._field.store_display_value or self._field.key in ('file', 'date'):
607
            return self._data.get(self._field.id)
607
            raw_value = self._data.get(self._field.id)
608
            return get_publisher().cache_complex_data(raw_value)
608 609
        raise AttributeError('raw')
609 610

  
610 611
    def get_value(self):
......
620 621
    def __str__(self):
621 622
        value = self.get_value()
622 623
        if not isinstance(value, six.string_types):
623
            value = str(value)
624
            value = get_publisher().cache_complex_data(value)
624 625
        return force_str(value)
625 626

  
626 627
    def __nonzero__(self):
......
913 914

  
914 915
    def get_value(self):
915 916
        # don't give access to underlying data dictionary.
916
        return self._data.get('%s_display' % self._field.id, '---')
917
        value = self._data.get(str(self._field.id))
918
        str_value = self._data.get('%s_display' % self._field.id, '---')
919
        if not value:
920
            return str_value
921
        return get_publisher().cache_complex_data(value, str_value)
917 922

  
918 923
    def __getitem__(self, key):
919 924
        try:
wcs/wf/backoffice_fields.py
15 15
# along with this program; if not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import xml.etree.ElementTree as ET
18
from quixote import get_publisher
18 19
from quixote.html import htmltext
19 20

  
20 21
from ..qommon import _, N_
......
123 124
                # assign empty value as None, as that will work for all field types
124 125
                new_value = None
125 126
            else:
126
                try:
127
                    new_value = self.compute(field['value'], raises=True,
128
                                             formdata=formdata, status_item=self)
129
                except:
130
                    continue
127
                with get_publisher().complex_data():
128
                    try:
129
                        new_value = self.compute(
130
                                field['value'],
131
                                raises=True,
132
                                allow_complex=formdef_field.allow_complex,
133
                                formdata=formdata,
134
                                status_item=self)
135
                    except Exception:
136
                        continue
137
                    if formdef_field.allow_complex:
138
                        complex_value = get_publisher().get_cached_complex_data(new_value)
139
                        if complex_value:
140
                            new_value = complex_value
131 141

  
132 142
            if formdef_field.convert_value_from_anything:
133 143
                try:
wcs/wf/create_formdata.py
405 405
            except KeyError:
406 406
                missing_fields.append(mapping.field_id)
407 407
                continue
408
            try:
409
                value = self.compute(mapping.expression, formdata=src, raises=True, status_item=self)
410
            except Exception:
411
                # already logged by self.compute
412
                continue
408
            with get_publisher().complex_data():
409
                try:
410
                    value = self.compute(
411
                            mapping.expression,
412
                            formdata=src,
413
                            raises=True,
414
                            allow_complex=dest_field.allow_complex,
415
                            status_item=self)
416
                except Exception:
417
                    # already logged by self.compute
418
                    continue
419
                if dest_field.allow_complex:
420
                    complex_value = get_publisher().get_cached_complex_data(value)
421
                    if complex_value:
422
                        value = complex_value
413 423

  
414 424
            try:
415 425
                self._set_value(
wcs/workflows.py
1965 1965
        return {'type': expression_type, 'value': expression_value}
1966 1966

  
1967 1967
    @classmethod
1968
    def compute(cls, var, render=True, raises=False, context=None, formdata=None, status_item=None):
1968
    def compute(cls, var, render=True, raises=False, allow_complex=False, context=None, formdata=None, status_item=None):
1969 1969
        if not isinstance(var, six.string_types):
1970 1970
            return var
1971 1971

  
......
1993 1993
                               exception=exception)
1994 1994

  
1995 1995
        if expression['type'] == 'template':
1996
            vars['allow_complex'] = allow_complex
1996 1997
            try:
1997 1998
                return Template(expression['value'], raises=raises, autoescape=False).render(vars)
1998 1999
            except TemplateError as e:
1999
-