Projet

Général

Profil

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

Nicolas Roche, 25 juin 2021 20:05

Télécharger (9,34 ko)

Voir les différences:

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

 .../migrations/0003_query_sort.py             | 25 +++++++++++++++++++
 passerelle/apps/opendatasoft/models.py        | 20 ++++++++++++---
 tests/test_opendatasoft.py                    | 17 ++++++++++---
 3 files changed, 56 insertions(+), 6 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='', 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

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

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

  
91 95
        result = []
......
100 104
        return result
101 105

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

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

  
155 168
    delete_view = 'opendatasoft-query-delete'
156 169
    edit_view = 'opendatasoft-query-edit'
157 170

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

  
170 184
    def as_endpoint(self):
171 185
        endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
172 186

  
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

  
......
190 191

  
191 192
@mock.patch('passerelle.utils.Request.get')
192 193
def test_search_using_q(mocked_get, app, connector):
193 194
    endpoint = utils.generic_endpoint_url('opendatasoft', 'search', slug=connector.slug)
194 195
    assert endpoint == '/opendatasoft/my_connector/search'
195 196
    params = {
196 197
        'dataset': 'referentiel-adresse-test',
197 198
        'text_template': '{{numero}} {{nom_rue}} {{nom_commun}}',
199
        'sort': '-nom_rue',
198 200
        'q': "rue de l'aubepine",
199 201
        'limit': 3,
200 202
    }
201 203
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
202 204
    resp = app.get(endpoint, params=params, status=200)
205
    assert mocked_get.call_args[1]['params'] == {
206
        'apikey': 'my_secret',
207
        'dataset': 'referentiel-adresse-test',
208
        'sort': '-nom_rue',
209
        'q': "rue de l'aubepine",
210
        'rows': '3',
211
    }
203 212
    assert not resp.json['err']
204 213
    assert len(resp.json['data']) == 3
205 214
    # check order is kept
206 215
    assert [x['id'] for x in resp.json['data']] == [
207 216
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
208 217
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
209 218
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
210 219
    ]
......
238 247
    endpoint = '/opendatasoft/my_connector/q/my_query/'
239 248
    params = {
240 249
        'q': "rue de l'aubepine",
241 250
        'limit': 3,
242 251
    }
243 252
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
244 253
    resp = app.get(endpoint, params=params, status=200)
245 254
    assert mocked_get.call_args[1]['params'] == {
246
        'dataset': 'referentiel-adresse-test',
247
        'q': "rue de l'aubepine",
248 255
        'apikey': 'my_secret',
249
        'rows': '3',
256
        'dataset': 'referentiel-adresse-test',
250 257
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
251 258
        'exclude.numero': ['42', '43'],
259
        'sort': '-nom_rue',
260
        'q': "rue de l'aubepine",
261
        'rows': '3',
252 262
    }
253 263
    assert not resp.json['err']
254 264
    assert len(resp.json['data']) == 3
255 265
    # check order is kept
256 266
    assert [x['id'] for x in resp.json['data']] == [
257 267
        'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
258 268
        '7cafcd5c692773e8b863587b2d38d6be82e023d8',
259 269
        '0984a5e1745701f71c91af73ce764e1f7132e0ff',
......
277 287
    mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_ID_SEARCH, status_code=200)
278 288
    resp = app.get(endpoint, params=params, status=200)
279 289
    assert mocked_get.call_args[1]['params'] == {
280 290
        'dataset': 'referentiel-adresse-test',
281 291
        'q': 'recordid:7cafcd5c692773e8b863587b2d38d6be82e023d8',
282 292
        'apikey': 'my_secret',
283 293
        'refine.source': ['Ville et Eurométropole de Strasbourg'],
284 294
        'exclude.numero': ['42', '43'],
295
        'sort': '-nom_rue',
285 296
    }
286 297
    assert len(resp.json['data']) == 1
287 298
    assert resp.json['data'][0]['text'] == "19 RUE DE L'AUBEPINE Lipsheim"
288 299

  
289 300

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