Projet

Général

Profil

0003-opendatasoft-add-facet-filters-50212.patch

Nicolas Roche, 25 juin 2021 19:33

Télécharger (9,81 ko)

Voir les différences:

Subject: [PATCH 3/3] opendatasoft: add facet filters (#50212)

 .../migrations/0002_auto_20210625_1852.py     | 33 +++++++++++++
 passerelle/apps/opendatasoft/models.py        | 48 +++++++++++++------
 tests/test_opendatasoft.py                    | 20 ++++++++
 3 files changed, 87 insertions(+), 14 deletions(-)
 create mode 100644 passerelle/apps/opendatasoft/migrations/0002_auto_20210625_1852.py
passerelle/apps/opendatasoft/migrations/0002_auto_20210625_1852.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2021-06-25 16:52
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('opendatasoft', '0001_initial'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='query',
17
            name='filter_expression',
18
            field=models.TextField(
19
                blank=True,
20
                help_text='Specify refine and exclude facet expressions separated lines',
21
                verbose_name='filter',
22
            ),
23
        ),
24
        migrations.AlterField(
25
            model_name='opendatasoft',
26
            name='service_url',
27
            field=models.CharField(
28
                help_text='URL without ending "api/records/1.0/search/"',
29
                max_length=256,
30
                verbose_name='Site URL',
31
            ),
32
        ),
33
    ]
passerelle/apps/opendatasoft/models.py
59 59
            Query.objects.filter(resource=instance).delete()
60 60
        for data_query in data_queries:
61 61
            query = Query.import_json(data_query)
62 62
            query.resource = instance
63 63
            queries.append(query)
64 64
        Query.objects.bulk_create(queries)
65 65
        return instance
66 66

  
67
    @endpoint(
68
        perm='can_access',
69
        description=_('Search'),
70
        parameters={
71
            'dataset': {'description': _('Dataset')},
72
            'text_template': {'description': _('Text template')},
73
            'id': {'description': _('Record identifier')},
74
            'q': {'description': _('Full text query')},
75
            'limit': {'description': _('Maximum items')},
76
        },
77
    )
78
    def search(self, request, dataset=None, text_template='', id=None, q=None, limit=None, **kwargs):
67
    def call_search(self, dataset=None, text_template='', filter_expression='', id=None, q=None, limit=None):
79 68
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.service_url)
80 69
        path = urlparse.urljoin(path, 'api/records/1.0/search/')
81 70
        url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
82 71

  
83 72
        if id is not None:
84 73
            query = 'recordid:%s' % id
85 74
        else:
86 75
            query = q
87 76
        params = {
88 77
            'dataset': dataset,
89 78
            'q': query,
90 79
        }
91 80
        if self.api_key:
92 81
            params.update({'apikey': self.api_key})
93 82
        if limit:
94 83
            params.update({'rows': limit})
84
        params.update(urlparse.parse_qs(filter_expression))
95 85

  
96 86
        result_response = self.requests.get(url, params=params)
97 87
        err_desc = result_response.json().get('error')
98 88
        if err_desc:
99 89
            raise APIError(err_desc, http_status=200)
100 90

  
101 91
        result = []
102 92
        for record in result_response.json().get('records'):
103 93
            data = {}
104 94
            for key, value in record.get('fields').items():
105 95
                data[key] = value
106 96
            data['id'] = record.get('recordid')
107 97
            data['text'] = render_to_string(text_template, data).strip()
108 98
            result.append(data)
109 99

  
100
        return result
101

  
102
    @endpoint(
103
        perm='can_access',
104
        description=_('Search'),
105
        parameters={
106
            'dataset': {'description': _('Dataset')},
107
            'text_template': {'description': _('Text template')},
108
            'id': {'description': _('Record identifier')},
109
            'q': {'description': _('Full text query')},
110
            'limit': {'description': _('Maximum items')},
111
        },
112
    )
113
    def search(self, request, dataset=None, text_template='', id=None, q=None, limit=None, **kwargs):
114
        result = self.call_search(dataset, text_template, '', id, q, limit)
110 115
        return {'data': result}
111 116

  
112 117
    @endpoint(
113 118
        name='q',
114 119
        description=_('Query'),
115 120
        pattern=r'^(?P<query_slug>[\w:_-]+)/$',
116 121
        perm='can_access',
117 122
        show=False,
118 123
    )
119 124
    def q(self, request, query_slug, **kwargs):
120 125
        query = get_object_or_404(Query, resource=self, slug=query_slug)
121
        return query.q(request, **kwargs)
126
        result = query.q(request, **kwargs)
127
        return {'data': result}
122 128

  
123 129
    def create_query_url(self):
124 130
        return reverse('opendatasoft-query-new', kwargs={'slug': self.slug})
125 131

  
126 132

  
127 133
class Query(BaseQuery):
128 134
    resource = models.ForeignKey(
129 135
        to=OpenDataSoft, related_name='queries', verbose_name=_('Resource'), on_delete=models.CASCADE
......
135 141
        help_text=_('dataset to query'),
136 142
    )
137 143
    text_template = models.TextField(
138 144
        verbose_name=_('Text template'),
139 145
        help_text=_("Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}"),
140 146
        validators=[validate_template],
141 147
        blank=True,
142 148
    )
149
    filter_expression = models.TextField(
150
        verbose_name=_('filter'),
151
        help_text=_('Specify refine and exclude facet expressions separated lines'),
152
        blank=True,
153
    )
143 154

  
144 155
    delete_view = 'opendatasoft-query-delete'
145 156
    edit_view = 'opendatasoft-query-edit'
146 157

  
147 158
    def q(self, request, **kwargs):
148
        return self.resource.search(request, dataset=self.dataset, text_template=self.text_template, **kwargs)
159
        return self.resource.call_search(
160
            dataset=self.dataset,
161
            text_template=self.text_template,
162
            filter_expression='&'.join(
163
                [x.strip() for x in str(self.filter_expression).splitlines() if x.strip()]
164
            ),
165
            id=kwargs.get('id'),
166
            q=kwargs.get('q'),
167
            limit=kwargs.get('limit'),
168
        )
149 169

  
150 170
    def as_endpoint(self):
151 171
        endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
152 172

  
153 173
        search_endpoint = self.resource.search.endpoint_info
154 174
        endpoint.func = search_endpoint.func
155 175
        endpoint.show_undocumented_params = False
156 176

  
tests/test_opendatasoft.py
138 138
def query(connector):
139 139
    return Query.objects.create(
140 140
        resource=connector,
141 141
        name='Référenciel adresses de test',
142 142
        slug='my_query',
143 143
        description='Rechercher une adresse',
144 144
        dataset='referentiel-adresse-test',
145 145
        text_template='{{numero}} {{nom_rue}} {{nom_commun}}',
146
        filter_expression='''
147
refine.source=Ville et Eurométropole de Strasbourg
148
exclude.numero=42
149
exclude.numero=43
150
''',
146 151
    )
147 152

  
148 153

  
149 154
def test_views(db, admin_user, app, connector):
150 155
    app = login(app)
151 156
    resp = app.get('/opendatasoft/my_connector/', status=200)
152 157
    resp = resp.click('New Query')
153 158
    resp.form['name'] = 'my query'
......
232 237
def test_query_q_using_q(mocked_get, app, query):
233 238
    endpoint = '/opendatasoft/my_connector/q/my_query/'
234 239
    params = {
235 240
        'q': "rue de l'aubepine",
236 241
        'limit': 3,
237 242
    }
238 243
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
239 244
    resp = app.get(endpoint, params=params, status=200)
245
    assert mocked_get.call_args[1]['params'] == {
246
        'dataset': 'referentiel-adresse-test',
247
        'q': "rue de l'aubepine",
248
        'apikey': 'my_secret',
249
        'rows': '3',
250
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
251
        'exclude.numero': ['42', '43'],
252
    }
240 253
    assert not resp.json['err']
241 254
    assert len(resp.json['data']) == 3
242 255
    # check order is kept
243 256
    assert [x['id'] for x in resp.json['data']] == [
244 257
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
245 258
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
246 259
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
247 260
    ]
......
258 271
@mock.patch('passerelle.utils.Request.get')
259 272
def test_query_q_using_id(mocked_get, app, query):
260 273
    endpoint = '/opendatasoft/my_connector/q/my_query/'
261 274
    params = {
262 275
        'id': '7cafcd5c692773e8b863587b2d38d6be82e023d8',
263 276
    }
264 277
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_ID_SEARCH, status_code=200)
265 278
    resp = app.get(endpoint, params=params, status=200)
279
    assert mocked_get.call_args[1]['params'] == {
280
        'dataset': 'referentiel-adresse-test',
281
        'q': 'recordid:7cafcd5c692773e8b863587b2d38d6be82e023d8',
282
        'apikey': 'my_secret',
283
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
284
        'exclude.numero': ['42', '43'],
285
    }
266 286
    assert len(resp.json['data']) == 1
267 287
    assert resp.json['data'][0]['text'] == "19 RUE DE L'AUBEPINE Lipsheim"
268 288

  
269 289

  
270 290
def test_opendatasoft_query_unicity(admin_user, app, connector, query):
271 291
    connector2 = OpenDataSoft.objects.create(
272 292
        slug='my_connector2',
273 293
        api_key='my_secret',
274
-