Projet

Général

Profil

0001-backoffice-add-possibility-to-display-linked-card-fi.patch

Frédéric Péters, 21 juillet 2020 15:03

Télécharger (23,8 ko)

Voir les différences:

Subject: [PATCH] backoffice: add possibility to display linked card fields as
 columns (#40036)

 tests/test_backoffice_pages.py      |  81 +++++++++++++++++++++
 wcs/backoffice/management.py        | 107 ++++++++++++++++++++++------
 wcs/forms/backoffice.py             |  15 ++--
 wcs/qommon/static/css/dc2/admin.css |  21 ++++++
 wcs/qommon/static/js/wcs.listing.js |  11 +++
 wcs/qommon/storage.py               |   2 +-
 wcs/sql.py                          |  64 +++++++++++++----
 7 files changed, 260 insertions(+), 41 deletions(-)
tests/test_backoffice_pages.py
126 126
    Workflow.wipe()
127 127
    Category.wipe()
128 128
    FormDef.wipe()
129
    CardDef.wipe()
129 130
    pub.custom_view_class.wipe()
130 131
    formdef = FormDef()
131 132
    formdef.name = 'form title'
......
646 647
    assert 'download?f=4&thumbnail=1' not in resp.text
647 648

  
648 649

  
650
def test_backoffice_card_field_columns(pub):
651
    user = create_superuser(pub)
652
    create_environment(pub)
653

  
654
    datasource = {
655
        'type': 'formula',
656
        'value': repr([('A', 'aa'), ('B', 'bb'), ('C', 'cc')])
657
    }
658

  
659
    CardDef.wipe()
660
    carddef = CardDef()
661
    carddef.name = 'foo'
662
    carddef.fields = [
663
        fields.CommentField(id='0', label='...', type='comment'),
664
        fields.StringField(id='1', label='Test', type='string', varname='foo'),
665
        fields.DateField(id='2', label='Date', type='date'),
666
        fields.BoolField(id='3', label='Bool', type='bool'),
667
        fields.ItemField(id='4', label='Item', type='item', data_source=datasource),
668
    ]
669
    carddef.backoffice_submission_roles = user.roles
670
    carddef.workflow_roles = {'_editor': user.roles[0]}
671
    carddef.digest_template = 'card {{form_var_foo}}'
672
    carddef.store()
673
    carddef.data_class().wipe()
674

  
675
    card = carddef.data_class()()
676
    card.data = {
677
        '1': 'plop',
678
        '2': time.strptime('2020-04-24', '%Y-%m-%d'),
679
        '3': True,
680
        '4': 'A',
681
        '4_display': 'aa',
682
    }
683
    card.store()
684

  
685
    formdef = FormDef.get_by_urlname('form-title')
686
    formdef.geolocations = {'base': 'Geolocation'}
687
    formdef.fields.append(
688
        fields.ItemField(id='4', label='card field', type='item',
689
            data_source={'type': 'carddef:foo', 'value': ''}))
690
    formdef.store()
691

  
692
    for formdata in formdef.data_class().select(lambda x: x.status == 'wf-new'):
693
        formdata.data['4'] = str(card.id)
694
        formdata.data['4_display'] = formdef.fields[-1].store_display_value(formdata.data, '4')
695
        formdata.data['4_structured'] = formdef.fields[-1].store_structured_value(formdata.data, '4')
696
        formdata.geolocations = {'base': {'lat': 48.83, 'lon': 2.32}}
697
        formdata.store()
698

  
699
    app = login(get_app(pub))
700
    resp = app.get('/backoffice/management/form-title/')
701
    assert resp.text.count('</th>') == 8  # six columns
702
    if not pub.is_using_postgresql():
703
        # no support for relation columns unless using SQL
704
        assert '4$1' not in resp.forms['listing-settings'].fields
705
        return
706
    assert '4$0' not in resp.forms['listing-settings'].fields
707
    resp.forms['listing-settings']['4$1'].checked = True
708
    resp.forms['listing-settings']['4$2'].checked = True
709
    resp.forms['listing-settings']['4$3'].checked = True
710
    resp.forms['listing-settings']['4$4'].checked = True
711
    resp = resp.forms['listing-settings'].submit()
712
    assert resp.text.count('</th>') == 12
713
    assert resp.text.count('data-link') == 17  # 17 rows
714
    assert resp.text.count('<td>plop</td>') == 17
715
    assert resp.text.count('<td>2020-04-24</td>') == 17
716
    assert resp.text.count('<td>Yes</td>') == 17
717
    assert resp.text.count('<td>aa</td>') == 17
718

  
719
    resp_csv = resp.click('Export as CSV File')
720
    assert resp_csv.text.splitlines()[1].endswith(',plop,2020-04-24,Yes,aa')
721

  
722
    resp_ods = resp.click('Export a Spreadsheet')
723

  
724
    resp_map = resp.click('Plot on a Map')
725
    geojson_url = re.findall(r'data-geojson-url="(.*?)"', resp_map.text)[0]
726
    resp_geojson = app.get(geojson_url)
727
    assert {'varname': None, 'label': 'card field - Test', 'value': 'plop', 'html_value': 'plop'} in resp_geojson.json['features'][0]['properties']['display_fields']
728

  
729

  
649 730
def test_backoffice_filter(pub):
650 731
    create_superuser(pub)
651 732
    create_environment(pub)
wcs/backoffice/management.py
1190 1190
        default_filters = self.get_default_filters(mode)
1191 1191

  
1192 1192
        filter_fields = []
1193
        for field in fake_fields + self.get_formdef_fields():
1193
        for field in fake_fields + list(self.get_formdef_fields()):
1194 1194
            field.enabled = False
1195 1195
            if field.type not in self.get_filterable_field_types() + ['status']:
1196 1196
                continue
......
1415 1415
                    return field_ids.index(x.id)
1416 1416
                return 9999
1417 1417

  
1418
            seen_parents = set()
1418 1419
            for field in sorted(self.get_formdef_fields(), key=get_column_position):
1419 1420
                if not hasattr(field, str('get_view_value')):
1420 1421
                    continue
1421
                r += htmltext('<li><span class="handle">⣿</span><label><input type="checkbox" name="%s"') % field.id
1422
                classnames = ''
1423
                attrs = ''
1424
                if isinstance(field, RelatedField):
1425
                    classnames = 'related-field'
1426
                    if field.parent_field.id in seen_parents:
1427
                        classnames += ' collapsed'
1428
                    attrs = 'data-relation-attr="%s"' % field.parent_field.id
1429
                elif getattr(field, 'has_relations', False):
1430
                    classnames = 'has-relations-field'
1431
                    attrs = 'data-field-id="%s"' % field.id
1432
                    seen_parents.add(field.id)
1433
                r += htmltext('<li class="%s" %s><span class="handle">⣿</span>' % (classnames, attrs))
1434
                r += htmltext('<label><input type="checkbox" name="%s"') % field.id
1422 1435
                if field.id in field_ids:
1423 1436
                    r += htmltext(' checked="checked"')
1424 1437
                r += htmltext('/>')
1425 1438
                r += htmltext('%s</label>') % misc.ellipsize(field.label, 70)
1439
                if getattr(field, 'has_relations', False):
1440
                    r += htmltext('<button class="expand-relations"></button>')
1426 1441
                r += htmltext('</li>')
1427 1442
                column_order.append(str(field.id))
1428 1443
            r += htmltext('</ul>')
......
1511 1526
            return redirect('..')
1512 1527

  
1513 1528
    def get_formdef_fields(self):
1514
        fields = []
1515
        fields.append(FakeField('id', 'id', _('Number')))
1529
        yield FakeField('id', 'id', _('Number'))
1516 1530
        if get_publisher().get_site_option('welco_url', 'variables'):
1517
            fields.append(FakeField('submission_channel', 'submission_channel', _('Channel')))
1531
            yield FakeField('submission_channel', 'submission_channel', _('Channel'))
1518 1532
        if self.formdef.backoffice_submission_roles:
1519
            fields.append(FakeField('submission_agent', 'submission_agent', _('Submission By')))
1520
        fields.append(FakeField('time', 'time', _('Created')))
1521
        fields.append(FakeField('last_update_time', 'last_update_time', _('Last Modified')))
1522
        fields.append(FakeField('user-label', 'user-label', _('User Label')))
1523
        fields.extend(self.formdef.get_all_fields())
1524
        fields.append(FakeField('status', 'status', _('Status')))
1525
        fields.append(FakeField('anonymised', 'anonymised', _('Anonymised')))
1533
            yield FakeField('submission_agent', 'submission_agent', _('Submission By'))
1534
        yield FakeField('time', 'time', _('Created'))
1535
        yield FakeField('last_update_time', 'last_update_time', _('Last Modified'))
1536
        yield FakeField('user-label', 'user-label', _('User Label'))
1537
        for field in self.formdef.get_all_fields():
1538
            yield field
1539
            if not get_publisher().is_using_postgresql():
1540
                continue
1541
            if not (field.type == 'item' and
1542
                    field.data_source and
1543
                    field.data_source.get('type', '').startswith('carddef:')):
1544
                continue
1545
            try:
1546
                carddef = CardDef.get_by_urlname(field.data_source['type'][8:])
1547
            except KeyError:
1548
                continue
1549
            for card_field in carddef.get_all_fields():
1550
                if not hasattr(card_field, 'get_view_value'):
1551
                    continue
1552
                field.has_relations = True
1553
                yield RelatedField(carddef, card_field, field)
1526 1554

  
1527
        return fields
1555
        yield FakeField('status', 'status', _('Status'))
1556
        yield FakeField('anonymised', 'anonymised', _('Anonymised'))
1528 1557

  
1529 1558
    def get_default_columns(self):
1530 1559
        if self.view:
......
1592 1621
            filters_dict.update(self.view.get_filters_dict())
1593 1622
        filters_dict.update(get_request().form)
1594 1623

  
1595
        for filter_field in fake_fields + self.get_formdef_fields():
1624
        for filter_field in fake_fields + list(self.get_formdef_fields()):
1596 1625
            if filter_field.type not in self.get_filterable_field_types():
1597 1626
                continue
1598 1627

  
......
1909 1938
                csv_output.writerow(self.formpage.csv_tuple_heading(self.fields))
1910 1939

  
1911 1940
                items, total_count = FormDefUI(self.formdef).get_listing_items(
1912
                        self.selected_filter, user=user, query=query,
1941
                        fields, self.selected_filter, user=user, query=query,
1913 1942
                        criterias=criterias)
1914 1943

  
1915 1944
                for filled in items:
......
2011 2040
                    ws.write(0, i, f)
2012 2041

  
2013 2042
                items, total_count = FormDefUI(self.formdef).get_listing_items(
2014
                        self.selected_filter, user=user, query=query,
2043
                        fields, self.selected_filter, user=user, query=query,
2015 2044
                        criterias=criterias)
2016 2045

  
2017 2046
                for i, filled in enumerate(items):
......
2076 2105
                    ws.write(0, i, f)
2077 2106

  
2078 2107
                items, total_count = FormDefUI(self.formdef).get_listing_items(
2079
                        self.selected_filter, user=user, query=query,
2108
                        fields, self.selected_filter, user=user, query=query,
2080 2109
                        criterias=criterias)
2081 2110

  
2082 2111
                for i, formdata in enumerate(items):
......
2132 2161
        if 'limit' in get_request().form:
2133 2162
            limit = misc.get_int_or_400(get_request().form['limit'])
2134 2163
        items, total_count = FormDefUI(self.formdef).get_listing_items(
2135
            selected_filter, user=user, query=query, criterias=criterias,
2164
            None, selected_filter, user=user, query=query, criterias=criterias,
2136 2165
            order_by=order_by, anonymise=anonymise, offset=offset, limit=limit)
2137 2166
        if get_publisher().is_using_postgresql():
2138 2167
            self.formdef.data_class().load_all_evolutions(items)
......
2175 2204
        query = get_request().form.get('q')
2176 2205

  
2177 2206
        items, total_count = FormDefUI(self.formdef).get_listing_items(
2178
                selected_filter, user=user, query=query, criterias=criterias)
2207
                fields, selected_filter, user=user, query=query, criterias=criterias)
2179 2208

  
2180 2209
        # only consider first key for now
2181 2210
        geoloc_key = list(self.formdef.geolocations.keys())[0]
......
2228 2257
                    raise errors.TraversalError()
2229 2258

  
2230 2259
                formdatas, total_count = FormDefUI(formdef).get_listing_items(
2231
                        selected_filter, user=user, query=query, criterias=criterias)
2260
                        fields, selected_filter, user=user, query=query, criterias=criterias)
2232 2261

  
2233 2262
                cal = vobject.iCalendar()
2234 2263
                cal.add('prodid').value = '-//Entr\'ouvert//NON SGML Publik'
......
3128 3157
        return [element]
3129 3158

  
3130 3159

  
3160
class RelatedField:
3161
    is_related_field = True
3162
    type = 'related-field'
3163
    store_display_value = None
3164
    varname = None
3165

  
3166
    def __init__(self, carddef, field, parent_field):
3167
        self.carddef = carddef
3168
        self.carddef_field = field
3169
        self.parent_field = parent_field
3170

  
3171
    @property
3172
    def id(self):
3173
        return '%s$%s' % (self.parent_field.id, self.carddef_field.id)
3174

  
3175
    @property
3176
    def label(self):
3177
        return '%s - %s' % (self.parent_field.label, self.carddef_field.label)
3178

  
3179
    def get_view_value(self, value, **kwargs):
3180
        if value is None:
3181
            return ''
3182
        if isinstance(value, bool):
3183
            return _('Yes') if value else _('No')
3184
        if isinstance(value, datetime.date):
3185
            return misc.strftime(misc.date_format(), value)
3186
        return value
3187

  
3188
    def get_view_short_value(self, value, max_len=30, **kwargs):
3189
        return self.get_view_value(value)
3190

  
3191
    def get_csv_heading(self):
3192
        return [self.label]
3193

  
3194
    def get_csv_value(self, value, **kwargs):
3195
        return [self.get_view_value(value)]
3196

  
3197

  
3131 3198
def do_graphs_section(period_start=None, period_end=None, criterias=None):
3132 3199
    from wcs import sql
3133 3200
    r = TemplateIO(html=True)
wcs/forms/backoffice.py
47 47
            if using_postgresql:
48 48
                criterias.append(Null('anonymised'))
49 49
            items, total_count = self.get_listing_items(
50
                            selected_filter, offset, limit, query, order_by,
50
                            fields, selected_filter, offset, limit, query, order_by,
51 51
                            criterias=criterias)
52 52
            if (offset > 0) or (total_count > limit > 0):
53 53
                partial_display = True
......
102 102
                    field_sort_key = 'receipt_time'
103 103
                elif f.id in ('user-label', 'submission_agent'):
104 104
                    field_sort_key = None
105
            elif hasattr(f, 'column_id'):
106
                field_sort_key = None
105 107
            else:
106 108
                field_sort_key = 'f%s' % f.id
107 109

  
......
178 180

  
179 181
        return item_ids
180 182

  
181
    def get_listing_items(self, selected_filter='all', offset=None,
183
    def get_listing_items(self, fields=None, selected_filter='all', offset=None,
182 184
            limit=None, query=None, order_by=None, user=None, criterias=None, anonymise=False):
183 185
        user = user or get_request().user
184 186
        formdata_class = self.formdef.data_class()
......
206 208
        if not offset:
207 209
            offset = 0
208 210

  
211
        kwargs = {}
212
        if get_publisher().is_using_postgresql():
213
            kwargs['fields'] = fields
214

  
209 215
        if limit:
210 216
            items = formdata_class.get_ids(item_ids[offset:offset+limit],
211
                    keep_order=True)
217
                    keep_order=True, **kwargs)
212 218
        else:
213
            items = formdata_class.get_ids(item_ids, keep_order=True)
219
            items = formdata_class.get_ids(item_ids, keep_order=True, **kwargs)
214 220

  
215 221
        return (items, total_count)
216 222

  
217

  
218 223
    def tbody(self, fields=None, items=None, url_action=None, include_checkboxes=False):
219 224
        r = TemplateIO(html=True)
220 225
        if url_action:
wcs/qommon/static/css/dc2/admin.css
1121 1121
	padding-left: 2em;
1122 1122
}
1123 1123

  
1124
ul.columns-filter li.collapsed {
1125
	display: none;
1126
}
1127

  
1128
ul.columns-filter button.expand-relations {
1129
	position: absolute;
1130
	right: 0;
1131
	top: 0;
1132
	height: 100%;
1133
	border: none;
1134
}
1135

  
1136
ul.columns-filter button.expand-relations::before {
1137
	font-family: FontAwesome;
1138
	content: "\f107";  /* angle-down */
1139
}
1140

  
1141
ul.columns-filter button.expand-relations.opened::before {
1142
	content: "\f106";  // angle-up
1143
}
1144

  
1124 1145
ul.multipage li {
1125 1146
	margin-left: 2em;
1126 1147
}
wcs/qommon/static/js/wcs.listing.js
213 213
    var dialog = $('<form>');
214 214
    var $dialog_filter = $('#columns-filter').clone().attr('id', null);
215 215
    $dialog_filter.appendTo(dialog);
216
    $dialog_filter.find('button.expand-relations').each(function(elem, i) {
217
      $(this).removeClass('opened');
218
      var field_id = $(this).parents('li.has-relations-field').data('field-id');
219
      $(this).parents('li').find('~ li[data-relation-attr=' + field_id + ']').addClass('collapsed');
220
    });
221
    $dialog_filter.find('button.expand-relations').on('click', function() {
222
      $(this).toggleClass('opened');
223
      var field_id = $(this).parents('li.has-relations-field').data('field-id');
224
      $(this).parents('li').find('~ li[data-relation-attr=' + field_id + ']').toggleClass('collapsed');
225
      return false;
226
    });
216 227
    $dialog_filter.sortable({handle: '.handle'})
217 228
    $(dialog).dialog({
218 229
            closeText: WCS_I18N.close,
wcs/qommon/storage.py
405 405
                                **kwargs)
406 406

  
407 407
    @classmethod
408
    def get_ids(cls, ids, ignore_errors=False, keep_order=False):
408
    def get_ids(cls, ids, ignore_errors=False, **kwargs):
409 409
        objects = []
410 410
        for x in ids:
411 411
            obj = cls.get(x, ignore_errors=ignore_errors)
wcs/sql.py
1138 1138

  
1139 1139
    @classmethod
1140 1140
    @guard_postgres
1141
    def get_ids(cls, ids, ignore_errors=False, keep_order=False):
1141
    def get_ids(cls, ids, ignore_errors=False, keep_order=False, fields=None):
1142 1142
        if not ids:
1143 1143
            return []
1144
        tables = [cls._table_name]
1145
        columns = ['%s.%s' % (cls._table_name, column_name) for column_name in
1146
                    [x[0] for x in cls._table_static_fields] + cls.get_data_fields()]
1147
        extra_fields = []
1148
        if fields:
1149
            # look for relations
1150
            for field in fields:
1151
                if not getattr(field, 'is_related_field', False):
1152
                    continue
1153
                carddef_dataclass = field.carddef.data_class()
1154
                carddef_table_alias = 't%s' % id(field.carddef)
1155
                carddef_table_decl = 'LEFT JOIN %s AS %s ON (CAST(%s.%s AS INTEGER) = %s.id)' % (
1156
                        carddef_dataclass._table_name,
1157
                        carddef_table_alias,
1158
                        cls._table_name,
1159
                        get_field_id(field.parent_field),
1160
                        carddef_table_alias)
1161
                if carddef_table_decl not in tables:
1162
                    tables.append(carddef_table_decl)
1163

  
1164
                column_field_id = get_field_id(field.carddef_field)
1165
                if field.carddef_field.store_display_value:
1166
                    column_field_id += '_display'
1167
                columns.append('%s.%s' % (carddef_table_alias, column_field_id))
1168
                extra_fields.append(field.id)
1169

  
1144 1170
        conn, cur = get_connection_and_cursor()
1145 1171
        sql_statement = '''SELECT %s
1146 1172
                             FROM %s
1147
                            WHERE id IN (%s)''' % (
1148
                                    ', '.join([x[0] for x in cls._table_static_fields]
1149
                                              + cls.get_data_fields()),
1173
                            WHERE %s.id IN (%s)''' % (
1174
                                    ', '.join(columns),
1175
                                    ' '.join(tables),
1150 1176
                                    cls._table_name,
1151 1177
                                    ','.join([str(x) for x in ids]))
1152 1178
        cur.execute(sql_statement)
1153
        objects = cls.get_objects(cur)
1179
        objects = cls.get_objects(cur, extra_fields=extra_fields)
1154 1180
        conn.commit()
1155 1181
        cur.close()
1156 1182
        if ignore_errors:
......
1163 1189
        return list(objects)
1164 1190

  
1165 1191
    @classmethod
1166
    def get_objects_iterator(cls, cur, ignore_errors=False):
1192
    def get_objects_iterator(cls, cur, ignore_errors=False, extra_fields=None):
1167 1193
        while True:
1168 1194
            row = cur.fetchone()
1169 1195
            if row is None:
1170 1196
                break
1171
            yield cls._row2ob(row)
1197
            yield cls._row2ob(row, extra_fields=extra_fields)
1172 1198

  
1173 1199
    @classmethod
1174
    def get_objects(cls, cur, ignore_errors=False, iterator=False):
1175
        generator = cls.get_objects_iterator(cur=cur, ignore_errors=ignore_errors)
1200
    def get_objects(cls, cur, ignore_errors=False, iterator=False, extra_fields=None):
1201
        generator = cls.get_objects_iterator(
1202
                cur=cur,
1203
                ignore_errors=ignore_errors,
1204
                extra_fields=extra_fields)
1176 1205
        if iterator:
1177 1206
            return generator
1178 1207
        return list(generator)
......
1665 1694
        cur.close()
1666 1695

  
1667 1696
    @classmethod
1668
    def _row2ob(cls, row):
1697
    def _row2ob(cls, row, extra_fields=None):
1669 1698
        o = cls()
1670 1699
        for static_field, value in zip(cls._table_static_fields,
1671 1700
                                       tuple(row[:len(cls._table_static_fields)])):
......
1689 1718
                                     'lat': float(m.group(2))}
1690 1719

  
1691 1720
        o.data = cls._row2obdata(row, cls._formdef)
1721
        if extra_fields:
1722
            # extra fields are tuck at the end
1723
            for i, field_id in enumerate(reversed(extra_fields)):
1724
                o.data[field_id] = row[-(i + 1)]
1725
            pass
1692 1726
        del o._last_update_time
1693 1727
        return o
1694 1728

  
......
1940 1974
        cur.close()
1941 1975

  
1942 1976
    @classmethod
1943
    def _row2ob(cls, row):
1977
    def _row2ob(cls, row, **kwargs):
1944 1978
        o = cls()
1945 1979
        (o.id, o.name, o.email, o.roles, o.is_admin, o.anonymous,
1946 1980
         o.name_identifiers, o.verified_fields, o.lasso_dump,
......
2084 2118
        cur.close()
2085 2119

  
2086 2120
    @classmethod
2087
    def _row2ob(cls, row):
2121
    def _row2ob(cls, row, **kwargs):
2088 2122
        o = cls.__new__(cls)
2089 2123
        cls.id = str_encode(row[0])
2090 2124
        session_data = pickle_loads(row[1])
......
2189 2223
        cur.close()
2190 2224

  
2191 2225
    @classmethod
2192
    def _row2ob(cls, row):
2226
    def _row2ob(cls, row, **kwargs):
2193 2227
        o = cls()
2194 2228
        (o.id, o.formdef_id, o.formdata_id) = [str_encode(x) for x in tuple(row[:3])]
2195 2229
        return o
......
2264 2298
        cur.close()
2265 2299

  
2266 2300
    @classmethod
2267
    def _row2ob(cls, row):
2301
    def _row2ob(cls, row, **kwargs):
2268 2302
        o = cls()
2269 2303
        for field, value in zip(cls._table_static_fields, tuple(row)):
2270 2304
            if field[1] == 'varchar':
......
2316 2350
        return super(AnyFormData, cls).get_objects(*args, **kwargs)
2317 2351

  
2318 2352
    @classmethod
2319
    def _row2ob(cls, row):
2353
    def _row2ob(cls, row, **kwargs):
2320 2354
        formdef_id = row[1]
2321 2355
        from wcs.formdef import FormDef
2322 2356
        formdef = cls._formdef_cache.setdefault(formdef_id, FormDef.get(formdef_id))
2323
-