Projet

Général

Profil

0001-cards-add-shared-and-datasource-custom-views-to-card.patch

Lauréline Guérin, 19 octobre 2020 15:53

Télécharger (16,4 ko)

Voir les différences:

Subject: [PATCH] cards: add shared and datasource custom views to carddef
 export (#42571)

 tests/test_carddef.py        | 73 ++++++++++++++++++++++++++++++++++++
 tests/test_formdef_import.py | 64 +++++++++++++++++++++++++++++++
 tests/test_snapshots.py      | 39 ++++++++++++++++++-
 wcs/custom_views.py          | 65 +++++++++++++++++++++++++++++++-
 wcs/formdef.py               | 19 ++++++++++
 5 files changed, 257 insertions(+), 3 deletions(-)
tests/test_carddef.py
77 77
            data_source={'type': 'carddef:foo'})
78 78
    ]
79 79
    carddef.store()
80

  
81
    # define also custom views
82
    pub.custom_view_class.wipe()
83

  
84
    custom_view = pub.custom_view_class()
85
    custom_view.title = 'datasource card view'
86
    custom_view.formdef = carddef
87
    custom_view.columns = {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}, {'id': '1'}, {'id': '2'}]}
88
    custom_view.filters = {'filter': 'recorded', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'a'}
89
    custom_view.visibility = 'datasource'
90
    custom_view.order_by = '-receipt_time'
91
    custom_view.store()
92

  
93
    custom_view = pub.custom_view_class()
94
    custom_view.title = 'shared card view'
95
    custom_view.formdef = carddef
96
    custom_view.columns = {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
97
    custom_view.filters = {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
98
    custom_view.visibility = 'any'
99
    custom_view.order_by = 'receipt_time'
100
    custom_view.store()
101

  
102
    custom_view = pub.custom_view_class()
103
    custom_view.title = 'private card view'
104
    custom_view.formdef = carddef
105
    custom_view.columns = {'list': [{'id': 'id'}]}
106
    custom_view.filters = {}
107
    custom_view.visibility = 'owner'
108
    custom_view.usier_id = 42
109
    custom_view.order_by = 'id'
110
    custom_view.store()
111

  
80 112
    carddef_xml = carddef.export_to_xml()
81 113
    assert carddef_xml.tag == 'carddef'
82 114
    carddef.data_class().wipe()
115
    pub.custom_view_class.wipe()
83 116

  
84 117
    carddef2 = CardDef.import_from_xml(BytesIO(ET.tostring(carddef_xml)))
85 118
    assert carddef2.name == 'foo'
86 119
    assert carddef2.fields[1].data_source == {'type': 'carddef:foo'}
120
    assert carddef2._custom_views
121

  
122
    custom_views = sorted(carddef2._custom_views, key=lambda a: a.visibility)
123
    assert len(custom_views) == 2
124
    assert custom_views[0].title == 'shared card view'
125
    assert custom_views[0].slug == 'shared-card-view'
126
    assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
127
    assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
128
    assert custom_views[0].visibility == 'any'
129
    assert custom_views[0].order_by == 'receipt_time'
130
    assert custom_views[0].formdef_id is None
131
    assert custom_views[0].formdef_type is None
132
    assert custom_views[1].title == 'datasource card view'
133
    assert custom_views[1].slug == 'datasource-card-view'
134
    assert custom_views[1].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}, {'id': '1'}, {'id': '2'}]}
135
    assert custom_views[1].filters == {'filter': 'recorded', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'a'}
136
    assert custom_views[1].visibility == 'datasource'
137
    assert custom_views[1].order_by == '-receipt_time'
138
    assert custom_views[1].formdef_id is None
139
    assert custom_views[1].formdef_type is None
140

  
141
    carddef2.store()
142
    custom_views = sorted(pub.custom_view_class.select(), key=lambda a: a.visibility)
143
    assert len(custom_views) == 2
144
    assert custom_views[0].title == 'shared card view'
145
    assert custom_views[0].slug == 'shared-card-view'
146
    assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
147
    assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
148
    assert custom_views[0].visibility == 'any'
149
    assert custom_views[0].order_by == 'receipt_time'
150
    assert custom_views[0].formdef_id == carddef2.id
151
    assert custom_views[0].formdef_type == 'carddef'
152
    assert custom_views[1].title == 'datasource card view'
153
    assert custom_views[1].slug == 'datasource-card-view'
154
    assert custom_views[1].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}, {'id': '1'}, {'id': '2'}]}
155
    assert custom_views[1].filters == {'filter': 'recorded', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'a'}
156
    assert custom_views[1].visibility == 'datasource'
157
    assert custom_views[1].order_by == '-receipt_time'
158
    assert custom_views[1].formdef_id == carddef2.id
159
    assert custom_views[1].formdef_type == 'carddef'
87 160

  
88 161

  
89 162
def test_template_access(pub):
tests/test_formdef_import.py
673 673
    f2 = FormDef.import_from_xml_tree(formdef_xml)
674 674
    assert len(f2.fields) == len(formdef.fields)
675 675
    assert f2.fields[0].prefill == {'type': 'string', 'value': None}
676

  
677

  
678
def test_custom_views():
679
    formdef = FormDef()
680
    formdef.name = 'foo'
681
    formdef.fields = [
682
        fields.StringField(id='1', label='Foo', type='string', varname='foo'),
683
        fields.StringField(id='2', label='Bar', type='string', varname='bar'),
684
    ]
685
    formdef.store()
686

  
687
    # define also custom views
688
    pub.custom_view_class.wipe()
689

  
690
    custom_view = pub.custom_view_class()
691
    custom_view.title = 'shared form view'
692
    custom_view.formdef = formdef
693
    custom_view.columns = {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
694
    custom_view.filters = {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
695
    custom_view.visibility = 'any'
696
    custom_view.order_by = 'receipt_time'
697
    custom_view.store()
698

  
699
    custom_view = pub.custom_view_class()
700
    custom_view.title = 'private form view'
701
    custom_view.formdef = formdef
702
    custom_view.columns = {'list': [{'id': 'id'}]}
703
    custom_view.filters = {}
704
    custom_view.visibility = 'owner'
705
    custom_view.usier_id = 42
706
    custom_view.order_by = 'id'
707
    custom_view.store()
708

  
709
    formdef_xml = formdef.export_to_xml()
710
    assert formdef_xml.tag == 'formdef'
711
    formdef.data_class().wipe()
712
    pub.custom_view_class.wipe()
713

  
714
    formdef2 = FormDef.import_from_xml(BytesIO(ET.tostring(formdef_xml)))
715
    assert formdef2.name == 'foo'
716
    assert formdef2._custom_views
717

  
718
    custom_views = formdef2._custom_views
719
    assert len(custom_views) == 1
720
    assert custom_views[0].title == 'shared form view'
721
    assert custom_views[0].slug == 'shared-form-view'
722
    assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
723
    assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
724
    assert custom_views[0].visibility == 'any'
725
    assert custom_views[0].order_by == 'receipt_time'
726
    assert custom_views[0].formdef_id is None
727
    assert custom_views[0].formdef_type is None
728

  
729
    formdef2.store()
730
    custom_views = pub.custom_view_class.select()
731
    assert len(custom_views) == 1
732
    assert custom_views[0].title == 'shared form view'
733
    assert custom_views[0].slug == 'shared-form-view'
734
    assert custom_views[0].columns == {'list': [{'id': 'id'}, {'id': 'time'}, {'id': 'status'}]}
735
    assert custom_views[0].filters == {'filter': 'done', 'filter-1': 'on', 'filter-status': 'on', 'filter-1-value': 'b'}
736
    assert custom_views[0].visibility == 'any'
737
    assert custom_views[0].order_by == 'receipt_time'
738
    assert custom_views[0].formdef_id == formdef2.id
739
    assert custom_views[0].formdef_type == 'formdef'
tests/test_snapshots.py
242 242
    carddef.fields = []
243 243
    carddef.store()
244 244

  
245
    pub.custom_view_class.wipe()
246
    custom_view = pub.custom_view_class()
247
    custom_view.title = 'shared form view'
248
    custom_view.formdef = carddef
249
    custom_view.columns = {'list': [{'id': 'id'}]}
250
    custom_view.filters = {}
251
    custom_view.visibility = 'any'
252
    custom_view.store()
253

  
254
    # new version has custom views
255
    carddef.name = 'test 1'
256
    carddef.store()
257

  
258
    # delete custom views
259
    pub.custom_view_class.wipe()
260

  
245 261
    app = login(get_app(pub))
246 262

  
247 263
    resp = app.get('/backoffice/cards/%s/history/' % carddef.id)
......
250 266
    assert 'This card model is readonly' in resp
251 267
    resp = resp.click('Geolocation')
252 268
    assert [x[0].name for x in resp.form.fields.values() if x[0].tag == 'button'] == ['cancel']
269
    assert pub.custom_view_class.count() == 0  # custom views are not restore on preview
253 270

  
254 271

  
255 272
def test_datasource_snapshot_browse(pub):
......
277 294
    create_role()
278 295
    app = login(get_app(pub))
279 296

  
297
    pub.custom_view_class.wipe()
298
    custom_view = pub.custom_view_class()
299
    custom_view.title = 'shared form view'
300
    custom_view.formdef = formdef_with_history
301
    custom_view.columns = {'list': [{'id': 'id'}]}
302
    custom_view.filters = {}
303
    custom_view.visibility = 'any'
304
    custom_view.store()
305

  
306
    # version 5 has custom views
307
    formdef_with_history.name = 'testform 5'
308
    formdef_with_history.description = 'this is a description (5)'
309
    formdef_with_history.store()
310

  
311
    # delete custom views
312
    pub.custom_view_class.wipe()
313

  
280 314
    resp = app.get('/backoffice/forms/%s/history/' % formdef_with_history.id)
281
    snapshot = pub.snapshot_class.select_object_history(formdef_with_history)[2]
315
    snapshot = pub.snapshot_class.select_object_history(formdef_with_history)[0]
282 316
    resp = resp.click(href='%s/view/' % snapshot.id)
283 317
    assert 'This form is readonly' in resp
284 318
    resp = resp.click('Description')
285
    assert resp.form['description'].value == 'this is a description (2)'
319
    assert resp.form['description'].value == 'this is a description (5)'
286 320
    assert [x[0].name for x in resp.form.fields.values() if x[0].tag == 'button'] == ['cancel']
321
    assert pub.custom_view_class.count() == 0  # custom views are not restore on preview
287 322

  
288 323

  
289 324
def test_workflow_snapshot_browse(pub):
wcs/custom_views.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 xml.etree.ElementTree as ET
18

  
17 19
from django.utils.six.moves.urllib import parse as urlparse
20
from django.utils.encoding import force_text
18 21
from quixote import get_publisher
19 22

  
20 23
from wcs.carddef import CardDef
21 24
from wcs.formdef import FormDef
22 25
from wcs.qommon.storage import StorableObject, Equal
23 26
from wcs.qommon.misc import simplify
27
from .qommon.misc import xml_node_text
24 28

  
25 29

  
26 30
class CustomView(StorableObject):
......
36 40
    filters = None
37 41
    order_by = None
38 42

  
43
    xml_root_node = 'custom_view'
44

  
39 45
    @property
40 46
    def user(self):
41 47
        return get_publisher().user_class.get(self.user_id)
......
57 63
        self.formdef_type = value.xml_root_node
58 64

  
59 65
    def match(self, user, formdef):
60
        if self.visibility == 'owner' and self.user_id != str(user.id):
66
        if self.visibility == 'owner' and (user is None or self.user_id != str(user.id)):
61 67
            return False
62 68
        if self.formdef_type != formdef.xml_root_node:
63 69
            return False
......
134 140

  
135 141
    def get_default_filters(self):
136 142
        return [key[7:] for key in self.filters if key.startswith('filter-')]
143

  
144
    def export_to_xml(self, charset=None):
145
        root = ET.Element(self.xml_root_node)
146
        fields = [
147
            'title',
148
            'slug',
149
            'visibility',
150
            'filters',
151
            'columns',
152
            'order_by',
153
        ]
154
        for attribute in fields:
155
            if getattr(self, attribute, None) is not None:
156
                val = getattr(self, attribute)
157
                el = ET.SubElement(root, attribute)
158
                if attribute == 'columns':
159
                    for field_dict in self.columns.get('list') or []:
160
                        if not isinstance(field_dict, dict):
161
                            continue
162
                        for k, v in sorted(field_dict.items()):
163
                            text_value = force_text(v, charset, errors='replace')
164
                            ET.SubElement(el, k).text = text_value
165
                elif type(val) is dict:
166
                    for k, v in sorted(val.items()):
167
                        text_value = force_text(v, charset, errors='replace')
168
                        ET.SubElement(el, k).text = text_value
169
                elif isinstance(val, str):
170
                    el.text = force_text(val, charset, errors='replace')
171
                else:
172
                    el.text = str(val)
173
        return root
174

  
175
    def init_with_xml(self, elem, charset):
176
        fields = [
177
            'title',
178
            'slug',
179
            'visibility',
180
            'filters',
181
            'columns',
182
            'order_by',
183
        ]
184
        for attribute in fields:
185
            el = elem.find(attribute)
186
            if el is None:
187
                continue
188
            if attribute == 'filters':
189
                v = {}
190
                for e in el:
191
                    v[e.tag] = xml_node_text(e)
192
                setattr(self, attribute, v)
193
            elif attribute == 'columns':
194
                v = []
195
                for e in el:
196
                    v.append({e.tag: xml_node_text(e)})
197
                setattr(self, attribute, {'list': v})
198
            else:
199
                setattr(self, attribute, xml_node_text(el))
wcs/formdef.py
389 389
            from . import sql
390 390
            sql.do_formdef_tables(self, rebuild_views=True,
391 391
                    rebuild_global_views=True)
392
        self.store_related_custom_views()
392 393
        return t
393 394

  
395
    def store_related_custom_views(self):
396
        for view in getattr(self, '_custom_views', []):
397
            view.formdef = self
398
            view.store()
399

  
394 400
    def get_all_fields(self):
395 401
        return (self.fields or []) + self.workflow.get_backoffice_fields()
396 402

  
......
1000 1006
            else:
1001 1007
                pass # TODO: extend support to other types
1002 1008

  
1009
        custom_views = ET.SubElement(root, 'custom_views')
1010
        for view in get_publisher().custom_view_class.select():
1011
            if view.match(user=None, formdef=self):
1012
                custom_views.append(view.export_to_xml(charset=charset))
1013

  
1003 1014
        geolocations = ET.SubElement(root, 'geolocations')
1004 1015
        for geoloc_key, geoloc_label in (self.geolocations or {}).items():
1005 1016
            element = ET.SubElement(geolocations, 'geolocation')
......
1135 1146
                option_value.set_content(base64.decodebytes(force_bytes(option.find('content').text)))
1136 1147
            formdef.workflow_options[option.attrib.get('varname')] = option_value
1137 1148

  
1149
        formdef._custom_views = []
1150
        for view in tree.findall('custom_views/%s' % get_publisher().custom_view_class.xml_root_node):
1151
            view_o = get_publisher().custom_view_class()
1152
            view_o.init_with_xml(view, charset)
1153
            formdef._custom_views.append(view_o)
1154

  
1138 1155
        if tree.find('last_modification') is not None:
1139 1156
            node = tree.find('last_modification')
1140 1157
            formdef.last_modification_time = time.strptime(node.text, '%Y-%m-%d %H:%M:%S')
......
1474 1491
        if self.lightweight and 'fields' in odict:
1475 1492
            # will be stored independently
1476 1493
            del odict['fields']
1494
        if '_custom_views' in odict:
1495
            del odict['_custom_views']
1477 1496
        return odict
1478 1497

  
1479 1498
    def __setstate__(self, dict):
1480
-