Projet

Général

Profil

0001-opendatasoft-add-sort-field-54442.patch

Nicolas Roche, 28 juillet 2021 17:15

Télécharger (13,6 ko)

Voir les différences:

Subject: [PATCH] opendatasoft: add sort field (#54442)

 .../migrations/0003_query_sort.py             | 25 +++++
 passerelle/apps/opendatasoft/models.py        | 37 +++++---
 tests/test_opendatasoft.py                    | 93 ++++++++++++++++++-
 3 files changed, 138 insertions(+), 17 deletions(-)
 create mode 100644 passerelle/apps/opendatasoft/migrations/0003_query_sort.py
passerelle/apps/opendatasoft/migrations/0003_query_sort.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2021-06-25 17:01
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', '0002_auto_20210625_1852'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='query',
17
            name='sort',
18
            field=models.CharField(
19
                blank=True,
20
                help_text='Sorts results by the specified field. A minus sign - may be used to perform an ascending sort.',
21
                max_length=256,
22
                verbose_name='Sort field',
23
            ),
24
        ),
25
    ]
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
    def call_search(self, dataset=None, text_template='', filter_expression='', id=None, q=None, limit=None):
67
    def call_search(
68
        self, dataset=None, text_template='', filter_expression='', sort=None, id=None, q=None, limit=None
69
    ):
68 70
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.service_url)
69 71
        path = urlparse.urljoin(path, 'api/records/1.0/search/')
70 72
        url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
71 73

  
74
        params = {'dataset': dataset}
72 75
        if id is not None:
73
            query = 'recordid:%s' % id
74
        else:
75
            query = q
76
        params = {
77
            'dataset': dataset,
78
            'q': query,
79
        }
76
            params['q'] = 'recordid:%s' % id
77
        elif q is not None:
78
            params['q'] = q
79
        elif sort:
80
            params['sort'] = sort
80 81
        if self.api_key:
81
            params.update({'apikey': self.api_key})
82
            params['apikey'] = self.api_key
82 83
        if limit:
83
            params.update({'rows': limit})
84
            params['rows'] = limit
84 85
        params.update(urlparse.parse_qs(filter_expression))
85 86

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

  
91 92
        result = []
......
102 103
        return result
103 104

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

  
119 123
    @endpoint(
120 124
        name='q',
121 125
        description=_('Query'),
122 126
        pattern=r'^(?P<query_slug>[\w:_-]+)/$',
123 127
        perm='can_access',
124 128
        show=False,
......
148 152
        validators=[validate_template],
149 153
        blank=True,
150 154
    )
151 155
    filter_expression = models.TextField(
152 156
        verbose_name=_('filter'),
153 157
        help_text=_('Specify refine and exclude facet expressions separated lines'),
154 158
        blank=True,
155 159
    )
160
    sort = models.CharField(
161
        verbose_name=_('Sort field'),
162
        help_text=_(
163
            "Sorts results by the specified field. A minus sign - may be used to perform an ascending sort."
164
        ),
165
        max_length=256,
166
        blank=True,
167
    )
156 168

  
157 169
    delete_view = 'opendatasoft-query-delete'
158 170
    edit_view = 'opendatasoft-query-edit'
159 171

  
160 172
    def q(self, request, **kwargs):
161 173
        return self.resource.call_search(
162 174
            dataset=self.dataset,
163 175
            text_template=self.text_template,
164 176
            filter_expression='&'.join(
165 177
                [x.strip() for x in str(self.filter_expression).splitlines() if x.strip()]
166 178
            ),
179
            sort=self.sort,
167 180
            id=kwargs.get('id'),
168 181
            q=kwargs.get('q'),
169 182
            limit=kwargs.get('limit'),
170 183
        )
171 184

  
172 185
    def as_endpoint(self):
173 186
        endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
174 187

  
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
        sort='-nom_rue',
146 147
        filter_expression='''
147 148
refine.source=Ville et Eurométropole de Strasbourg
148 149
exclude.numero=42
149 150
exclude.numero=43
150 151
''',
151 152
    )
152 153

  
153 154

  
......
183 184
    # error returned by opendadasoft server
184 185
    json_response = json.dumps({'error': "The query is invalid : Field 00 doesn't exist"})
185 186
    mocked_get.return_value = utils.FakedResponse(content=json_response, status_code=200)
186 187
    resp = app.get(endpoint, status=200)
187 188
    assert resp.json['err']
188 189
    assert resp.json['err_desc'] == "The query is invalid : Field 00 doesn't exist"
189 190

  
190 191

  
192
@mock.patch('passerelle.utils.Request.get')
193
def test_search(mocked_get, app, connector):
194
    endpoint = utils.generic_endpoint_url('opendatasoft', 'search', slug=connector.slug)
195
    assert endpoint == '/opendatasoft/my_connector/search'
196
    params = {
197
        'dataset': 'referentiel-adresse-test',
198
        'text_template': '{{numero}} {{nom_rue}} {{nom_commun}}',
199
        'sort': '-nom_rue',
200
        'limit': 3,
201
    }
202
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
203
    resp = app.get(endpoint, params=params, status=200)
204
    assert mocked_get.call_args[1]['params'] == {
205
        'apikey': 'my_secret',
206
        'dataset': 'referentiel-adresse-test',
207
        'sort': '-nom_rue',
208
        'rows': '3',
209
    }
210
    assert not resp.json['err']
211
    assert len(resp.json['data']) == 3
212
    # check order is kept
213
    assert [x['id'] for x in resp.json['data']] == [
214
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
215
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
216
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
217
    ]
218
    # check text results
219
    assert [x['text'] for x in resp.json['data']] == [
220
        "33 RUE DE L'AUBEPINE Strasbourg",
221
        "19 RUE DE L'AUBEPINE Lipsheim",
222
        "29 RUE DE L'AUBEPINE Strasbourg",
223
    ]
224
    # check additional attributes
225
    assert [x['numero'] for x in resp.json['data']] == ['33', '19', '29']
226

  
227

  
191 228
@mock.patch('passerelle.utils.Request.get')
192 229
def test_search_using_q(mocked_get, app, connector):
193 230
    endpoint = utils.generic_endpoint_url('opendatasoft', 'search', slug=connector.slug)
194 231
    assert endpoint == '/opendatasoft/my_connector/search'
195 232
    params = {
196 233
        'dataset': 'referentiel-adresse-test',
197 234
        'text_template': '{{numero}} {{nom_rue}} {{nom_commun}}',
235
        'sort': '-nom_rue',
198 236
        'q': "rue de l'aubepine",
199 237
        'limit': 3,
200 238
    }
201 239
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
202 240
    resp = app.get(endpoint, params=params, status=200)
241
    assert mocked_get.call_args[1]['params'] == {
242
        'apikey': 'my_secret',
243
        'dataset': 'referentiel-adresse-test',
244
        'q': "rue de l'aubepine",
245
        'rows': '3',
246
    }
203 247
    assert not resp.json['err']
204 248
    assert len(resp.json['data']) == 3
205 249
    # check order is kept
206 250
    assert [x['id'] for x in resp.json['data']] == [
207 251
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
208 252
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
209 253
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
210 254
    ]
......
224 268
    assert endpoint == '/opendatasoft/my_connector/search'
225 269
    params = {
226 270
        'dataset': 'referentiel-adresse-test',
227 271
        'text_template': '{{numero}} {{nom_rue}} {{nom_commun}}',
228 272
        'id': '7cafcd5c692773e8b863587b2d38d6be82e023d8',
229 273
    }
230 274
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_ID_SEARCH, status_code=200)
231 275
    resp = app.get(endpoint, params=params, status=200)
276
    assert mocked_get.call_args[1]['params'] == {
277
        'apikey': 'my_secret',
278
        'dataset': 'referentiel-adresse-test',
279
        'q': 'recordid:7cafcd5c692773e8b863587b2d38d6be82e023d8',
280
    }
232 281
    assert len(resp.json['data']) == 1
233 282
    assert resp.json['data'][0]['text'] == "19 RUE DE L'AUBEPINE Lipsheim"
234 283

  
235 284

  
236 285
@mock.patch('passerelle.utils.Request.get')
237
def test_query_q_using_q(mocked_get, app, query):
286
def test_query_q(mocked_get, app, query):
238 287
    endpoint = '/opendatasoft/my_connector/q/my_query/'
239 288
    params = {
240
        'q': "rue de l'aubepine",
241 289
        'limit': 3,
242 290
    }
243 291
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
244 292
    resp = app.get(endpoint, params=params, status=200)
245 293
    assert mocked_get.call_args[1]['params'] == {
294
        'apikey': 'my_secret',
246 295
        'dataset': 'referentiel-adresse-test',
296
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
297
        'exclude.numero': ['42', '43'],
298
        'sort': '-nom_rue',
299
        'rows': '3',
300
    }
301
    assert not resp.json['err']
302
    assert len(resp.json['data']) == 3
303
    # check order is kept
304
    assert [x['id'] for x in resp.json['data']] == [
305
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
306
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
307
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
308
    ]
309
    # check text results
310
    assert [x['text'] for x in resp.json['data']] == [
311
        "33 RUE DE L'AUBEPINE Strasbourg",
312
        "19 RUE DE L'AUBEPINE Lipsheim",
313
        "29 RUE DE L'AUBEPINE Strasbourg",
314
    ]
315
    # check additional attributes
316
    assert [x['numero'] for x in resp.json['data']] == ['33', '19', '29']
317

  
318

  
319
@mock.patch('passerelle.utils.Request.get')
320
def test_query_q_using_q(mocked_get, app, query):
321
    endpoint = '/opendatasoft/my_connector/q/my_query/'
322
    params = {
247 323
        'q': "rue de l'aubepine",
324
        'limit': 3,
325
    }
326
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
327
    resp = app.get(endpoint, params=params, status=200)
328
    assert mocked_get.call_args[1]['params'] == {
248 329
        'apikey': 'my_secret',
249
        'rows': '3',
330
        'dataset': 'referentiel-adresse-test',
250 331
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
251 332
        'exclude.numero': ['42', '43'],
333
        'q': "rue de l'aubepine",
334
        'rows': '3',
252 335
    }
253 336
    assert not resp.json['err']
254 337
    assert len(resp.json['data']) == 3
255 338
    # check order is kept
256 339
    assert [x['id'] for x in resp.json['data']] == [
257 340
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
258 341
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
259 342
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
......
272 355
def test_query_q_using_id(mocked_get, app, query):
273 356
    endpoint = '/opendatasoft/my_connector/q/my_query/'
274 357
    params = {
275 358
        'id': '7cafcd5c692773e8b863587b2d38d6be82e023d8',
276 359
    }
277 360
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_ID_SEARCH, status_code=200)
278 361
    resp = app.get(endpoint, params=params, status=200)
279 362
    assert mocked_get.call_args[1]['params'] == {
280
        'dataset': 'referentiel-adresse-test',
281
        'q': 'recordid:7cafcd5c692773e8b863587b2d38d6be82e023d8',
282 363
        'apikey': 'my_secret',
364
        'dataset': 'referentiel-adresse-test',
283 365
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
284 366
        'exclude.numero': ['42', '43'],
367
        'q': 'recordid:7cafcd5c692773e8b863587b2d38d6be82e023d8',
285 368
    }
286 369
    assert len(resp.json['data']) == 1
287 370
    assert resp.json['data'][0]['text'] == "19 RUE DE L'AUBEPINE Lipsheim"
288 371

  
289 372

  
290 373
def test_opendatasoft_query_unicity(admin_user, app, connector, query):
291 374
    connector2 = OpenDataSoft.objects.create(
292 375
        slug='my_connector2',
293
-