Projet

Général

Profil

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

Nicolas Roche, 20 juillet 2021 12:12

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 = []
......
102 106
        return result
103 107

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

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

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

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

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

  
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
-