Projet

Général

Profil

0002-base_adresse-add-API-G-o-endpoints-11497.patch

Valentin Deniaud, 28 novembre 2019 14:25

Télécharger (18,1 ko)

Voir les différences:

Subject: [PATCH 2/2] =?UTF-8?q?base=5Fadresse:=20add=20API=20G=C3=A9o=20en?=
 =?UTF-8?q?dpoints=20(#11497)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Namely /cities/, /counties/ and /regions/.
 .../migrations/0015_auto_20191128_1416.py     |  30 +++
 passerelle/apps/base_adresse/models.py        | 140 +++++++++++-
 tests/test_base_adresse.py                    | 216 ++++++++++++++++++
 3 files changed, 383 insertions(+), 3 deletions(-)
 create mode 100644 passerelle/apps/base_adresse/migrations/0015_auto_20191128_1416.py
passerelle/apps/base_adresse/migrations/0015_auto_20191128_1416.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2019-11-28 13:16
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
        ('base_adresse', '0014_auto_20190207_0456'),
12
    ]
13

  
14
    operations = [
15
        migrations.AddField(
16
            model_name='baseadresse',
17
            name='api_geo_url',
18
            field=models.CharField(default=b'https://geo.api.gouv.fr/', help_text='Base Adresse API Geo URL', max_length=128, verbose_name='API Geo URL'),
19
        ),
20
        migrations.AddField(
21
            model_name='baseadresse',
22
            name='county',
23
            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='County code to get cities from'),
24
        ),
25
        migrations.AddField(
26
            model_name='baseadresse',
27
            name='region',
28
            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Region code to get cities and counties from'),
29
        ),
30
    ]
passerelle/apps/base_adresse/models.py
23 23
        verbose_name=_('Service URL'),
24 24
        help_text=_('Base Adresse Web Service URL'))
25 25

  
26
    api_geo_url = models.CharField(
27
        max_length=128, blank=False,
28
        default='https://geo.api.gouv.fr/',
29
        verbose_name=_('API Geo URL'),
30
        help_text=_('Base Adresse API Geo URL'))
31

  
26 32
    category = _('Geographic information system')
27 33

  
28
    api_description = _("The API is a partial view of OpenStreetMap's Nominatim "
29
                        "own API; it currently doesn't support all parameters and "
30
                        "is limited to the JSON format.")
34
    api_description = _("The geocoding endpoints are a partial view of OpenStreetMap's "
35
                        "Nominatim own API; it currently doesn't support all parameters and "
36
                        "is limited to the JSON format. The cities, counties and regions "
37
                        "endpoints source data from API Geo.")
31 38

  
32 39
    zipcode = models.CharField(
33 40
        max_length=600,
34 41
        blank=True,
35 42
        verbose_name=_('Postal codes or county number to get streets, separated with commas'))
36 43

  
44
    county = models.PositiveSmallIntegerField(
45
        blank=True,
46
        null=True,
47
        verbose_name=_('County code to get cities from'))
48

  
49
    region = models.PositiveSmallIntegerField(
50
        blank=True,
51
        null=True,
52
        verbose_name=_('Region code to get cities and counties from'))
53

  
37 54
    class Meta:
38 55
        verbose_name = _('Base Adresse Web Service')
39 56

  
......
152 169

  
153 170
        return {'data': result}
154 171

  
172
    @endpoint(description=_('Cities list'),
173
              parameters={
174
                  'zipcode': {'description': _('Zipcode'), 'example_value': '75014'},
175
                  'q': {'description': _("City's name"), 'example_value': 'Paris'},
176
                  'code': {'description': _('City code (INSEE)'), 'example_value': '75056'},
177
                  'region_code': {'description': _('Region code'), 'example_value': '11'},
178
                  'county_code': {'description': _('County code'), 'example_value': '75'},
179
              })
180
    def cities(self, request, id=None, zipcode=None, q=None, code=None,
181
               region_code=None, county_code=None, **kwargs):
182
        if id is not None:
183
            code, zipcode = id.split('.')
184

  
185
        request_data = {
186
            'format': 'json',
187
            'boost': 'population',
188
            'fields': 'nom,code,codesPostaux,surface,codeDepartement,'
189
            'departement,codeRegion,region,population',
190
        }
191

  
192
        if code:
193
            request_data['code'] = code
194
        if zipcode:
195
            request_data['codePostal'] = zipcode
196
        if q:
197
            request_data['nom'] = q
198
        if self.county or county_code:
199
            request_data['codeDepartement'] = county_code or self.county
200
        if self.region or region_code:
201
            request_data['codeRegion'] = region_code or self.region
202

  
203
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.api_geo_url)
204
        path = os.path.join(path, 'communes/')
205
        query = urlencode(request_data)
206
        url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
207

  
208
        result_response = self.requests.get(url)
209

  
210
        data = []
211
        for city in result_response.json():
212
            zipcodes = [zipcode] if zipcode else city['codesPostaux']
213
            for zipcode in zipcodes:
214
                new_city = city.copy()
215
                new_city['codePostal'] = zipcode
216
                new_city['id'] = '.'.join((city['code'], zipcode))
217
                new_city['text'] = '%s (%s)' % (city['nom'], zipcode)
218
                data.append(new_city)
219

  
220
        return {'data': data}
221

  
222
    @endpoint(description=_('Counties list'),
223
              parameters={
224
                  'q': {'description': _('County name'), 'example_value': 'Nord'},
225
                  'code': {'description': _('County code'), 'example_value': '59'},
226
                  'region_code': {'description': _('Region code'), 'example_value': '32'},
227
              })
228
    def counties(self, request, id=None, q=None, code=None, region_code=None, **kwargs):
229
        if id is not None:
230
            code = id
231

  
232
        request_data = {
233
            'fields': 'nom,code,codeRegion,region',
234
        }
235

  
236
        if code:
237
            request_data['code'] = code
238
        if q:
239
            request_data['nom'] = q
240
        if self.region or region_code:
241
            request_data['codeRegion'] = region_code or self.region
242

  
243
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.api_geo_url)
244
        path = os.path.join(path, 'departements/')
245
        query = urlencode(request_data)
246
        url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
247

  
248
        result_response = self.requests.get(url)
249

  
250
        data = result_response.json()
251
        for county in data:
252
            county['id'] = county['code']
253
            county['text'] = '%s (%s)' % (county['nom'], county['code'])
254

  
255
        return {'data': data}
256

  
257
    @endpoint(description=_('Regions list'),
258
              parameters={
259
                  'q': {'description': _('Region name'), 'example_value': 'Hauts-de-France'},
260
                  'code': {'description': _('Region code'), 'example_value': '32'},
261
              })
262
    def regions(self, request, id=None, q=None, code=None, **kwargs):
263
        if id is not None:
264
            code = id
265

  
266
        request_data = {
267
            'fields': 'nom,code',
268
        }
269

  
270
        if code:
271
            request_data['code'] = code
272
        if q:
273
            request_data['nom'] = q
274

  
275
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.api_geo_url)
276
        path = os.path.join(path, 'regions/')
277
        query = urlencode(request_data)
278
        url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
279

  
280
        result_response = self.requests.get(url)
281

  
282
        data = result_response.json()
283
        for region in data:
284
            region['id'] = region['code']
285
            region['text'] = region['nom']
286

  
287
        return {'data': data}
288

  
155 289
    def check_status(self):
156 290
        if self.service_url == 'https://api-adresse.data.gouv.fr/':
157 291
            result = self.search(None, '169 rue du chateau, paris')
tests/test_base_adresse.py
7 7
import json
8 8

  
9 9
from django.core.management import call_command
10
from django.utils.six.moves.urllib.parse import quote
10 11

  
11 12
from passerelle.apps.base_adresse.models import BaseAdresse, StreetModel
12 13

  
......
41 42

  
42 43
FAKE_DATA = ''
43 44

  
45
FAKE_API_GEO = json.dumps([{
46
    "code": "75056",
47
    "codeDepartement": "75",
48
    "codeRegion": "11",
49
    "codesPostaux": [
50
        "75001",
51
        "75002",
52
    ],
53
    "departement": {
54
        "code": "75",
55
        "nom": "Paris"
56
    },
57
    "nom": "Paris",
58
    "population": 2190327,
59
    "region": {
60
        "code": "11",
61
        "nom": "Île-de-France"
62
    },
63
    "surface": 10528.81
64
}])
65

  
66
FAKE_API_GEO_COUNTIES = [
67
    {
68
        "code": "59",
69
        "codeRegion": "32",
70
        "nom": "Nord",
71
        "region": {
72
            "code": "32",
73
            "nom": "Hauts-de-France"
74
        }
75
    },
76
    {
77
        "code": "58",
78
        "codeRegion": "27",
79
        "nom": "Nievre",
80
        "region": {
81
            "code": "27",
82
            "nom": "Bourgogne-Franche-Comté"
83
        }
84
    }
85
]
86

  
87
FAKE_API_GEO_REGIONS = [
88
    {
89
        "code": "32",
90
        "nom": "Hauts-de-France"
91
    },
92
    {
93
        "code": "24",
94
        "nom": "Centre-Val de Loire"
95
    }
96
]
97

  
44 98

  
45 99
@pytest.fixture
46 100
def base_adresse(db):
......
258 312
    mocked_get.assert_any_call('http://bano.openstreetmap.fr/BAN_odbl/BAN_odbl_2A-json.bz2')
259 313
    mocked_get.assert_any_call('http://bano.openstreetmap.fr/BAN_odbl/BAN_odbl_2B-json.bz2')
260 314
    assert StreetModel.objects.count() == 5
315

  
316

  
317
@mock.patch('passerelle.utils.Request.get')
318
def test_base_adresse_cities(mocked_get, app, base_adresse):
319
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
320
                                          slug=base_adresse.slug)
321
    assert endpoint == '/base-adresse/base-adresse/cities'
322
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
323
    resp = app.get(endpoint, params={}, status=200)
324
    data = resp.json['data']
325
    assert len(data) == 2
326
    paris1, paris2 = data
327
    assert paris1.pop('id') == '75056.75001'
328
    assert paris2.pop('id') == '75056.75002'
329
    assert paris1.pop('codePostal') == '75001'
330
    assert paris2.pop('codePostal') == '75002'
331
    assert paris1.pop('text') == 'Paris (75001)'
332
    assert paris2.pop('text') == 'Paris (75002)'
333
    assert paris1 == paris2
334

  
335

  
336
@mock.patch('passerelle.utils.Request.get')
337
def test_base_adresse_cities_zipcode(mocked_get, app, base_adresse):
338
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
339
                                          slug=base_adresse.slug)
340
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
341
    resp = app.get(endpoint, params={'zipcode': 75002}, status=200)
342
    data = resp.json['data']
343
    assert len(data) == 1
344
    assert data[0]['id'] == '75056.75002'
345

  
346

  
347
@mock.patch('passerelle.utils.Request.get')
348
def test_base_adresse_cities_id(mocked_get, app, base_adresse):
349
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
350
                                          slug=base_adresse.slug)
351
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
352
    resp = app.get(endpoint, params={'zipcode': 75002, 'code': 75056}, status=200)
353
    data = resp.json['data']
354
    assert len(data) == 1
355

  
356
    resp = app.get(endpoint, params={'id': data[0]['id']}, status=200)
357
    new_data = resp.json['data']
358
    assert new_data == data
359

  
360

  
361
@mock.patch('passerelle.utils.Request.get')
362
def test_base_adresse_cities_path(mocked_get, app, base_adresse):
363
    base_adresse.api_geo_url = 'http://example.net/path/'
364
    base_adresse.save()
365
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
366
                                          slug=base_adresse.slug)
367
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
368
    resp = app.get(endpoint, params={}, status=200)
369
    assert mocked_get.call_args[0][0].startswith('http://example.net/path/communes/?')
370

  
371

  
372
@mock.patch('passerelle.utils.Request.get')
373
def test_base_adresse_cities_qs(mocked_get, app, base_adresse):
374
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
375
                                          slug=base_adresse.slug)
376
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
377
    resp = app.get(endpoint, params={}, status=200)
378
    assert 'format=json' in mocked_get.call_args[0][0]
379
    assert 'boost=population' in mocked_get.call_args[0][0]
380
    assert 'fields=' + quote('nom,code,codesPostaux,surface,codeDepartement,'
381
        'departement,codeRegion,region,population') in mocked_get.call_args[0][0]
382
    assert not 'codeRegion=' in mocked_get.call_args[0][0]
383
    assert not 'codeDepartement=' in mocked_get.call_args[0][0]
384

  
385

  
386
@mock.patch('passerelle.utils.Request.get')
387
def test_base_adresse_cities_qs_county_region(mocked_get, app, base_adresse):
388
    base_adresse.county = 75
389
    base_adresse.region = 11
390
    base_adresse.save()
391
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
392
                                          slug=base_adresse.slug)
393
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
394
    resp = app.get(endpoint, params={}, status=200)
395
    assert 'codeRegion=11' in mocked_get.call_args[0][0]
396
    assert 'codeDepartement=75' in mocked_get.call_args[0][0]
397

  
398
    resp = app.get(endpoint, params={'county_code': 42, 'region_code': 42}, status=200)
399
    assert 'codeRegion=42' in mocked_get.call_args[0][0]
400
    assert 'codeDepartement=42' in mocked_get.call_args[0][0]
401

  
402

  
403
@mock.patch('passerelle.utils.Request.get')
404
def test_base_adresse_cities_qs_filter(mocked_get, app, base_adresse):
405
    endpoint = utils.generic_endpoint_url('base-adresse', 'cities',
406
                                          slug=base_adresse.slug)
407
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=200)
408
    resp = app.get(endpoint, params={'q': 'Par', 'zipcode': 75001, 'code': 75056},
409
                   status=200)
410
    assert 'nom=Par' in mocked_get.call_args[0][0]
411
    assert 'codePostal=75001' in mocked_get.call_args[0][0]
412
    assert 'code=75056' in mocked_get.call_args[0][0]
413

  
414
    resp = app.get(endpoint, params={'id': '75056.75001'}, status=200)
415
    assert 'codePostal=75001' in mocked_get.call_args[0][0]
416
    assert 'code=75056' in mocked_get.call_args[0][0]
417

  
418

  
419
@mock.patch('passerelle.utils.Request.get')
420
def test_base_adresse_counties(mocked_get, app, base_adresse):
421
    endpoint = utils.generic_endpoint_url('base-adresse', 'counties',
422
                                          slug=base_adresse.slug)
423
    assert endpoint == '/base-adresse/base-adresse/counties'
424
    mocked_get.return_value = utils.FakedResponse(
425
        content=json.dumps(FAKE_API_GEO_COUNTIES), status_code=200)
426
    resp = app.get(endpoint, params={}, status=200)
427
    data = resp.json['data']
428
    assert data[0]['id'] == '59'
429
    assert data[0]['text'] == 'Nord (59)'
430
    assert data[1]['id'] == '58'
431
    assert data[1]['text'] == 'Nievre (58)'
432

  
433

  
434
@mock.patch('passerelle.utils.Request.get')
435
def test_base_adresse_counties_qs(mocked_get, app, base_adresse):
436
    endpoint = utils.generic_endpoint_url('base-adresse', 'counties',
437
                                          slug=base_adresse.slug)
438
    mocked_get.return_value = utils.FakedResponse(
439
        content=json.dumps([FAKE_API_GEO_COUNTIES[0]]), status_code=200)
440
    resp = app.get(endpoint, params={'code': '59', 'q': 'N', 'region_code': '32'}, status=200)
441
    assert 'fields=' + quote('nom,code,codeRegion,region') in mocked_get.call_args[0][0]
442
    assert 'nom=N' in mocked_get.call_args[0][0]
443
    assert 'code=59' in mocked_get.call_args[0][0]
444
    assert 'codeRegion=32' in mocked_get.call_args[0][0]
445

  
446
    resp = app.get(endpoint, params={'id': '59'}, status=200)
447
    assert 'code=59' in mocked_get.call_args[0][0]
448

  
449

  
450
@mock.patch('passerelle.utils.Request.get')
451
def test_base_adresse_regions(mocked_get, app, base_adresse):
452
    endpoint = utils.generic_endpoint_url('base-adresse', 'regions',
453
                                          slug=base_adresse.slug)
454
    assert endpoint == '/base-adresse/base-adresse/regions'
455
    mocked_get.return_value = utils.FakedResponse(
456
        content=json.dumps(FAKE_API_GEO_REGIONS), status_code=200)
457
    resp = app.get(endpoint, params={}, status=200)
458
    data = resp.json['data']
459
    assert data[0]['id'] == '32'
460
    assert data[0]['text'] == 'Hauts-de-France'
461
    assert data[1]['id'] == '24'
462
    assert data[1]['text'] == 'Centre-Val de Loire'
463

  
464

  
465
@mock.patch('passerelle.utils.Request.get')
466
def test_base_adresse_regions_qs(mocked_get, app, base_adresse):
467
    endpoint = utils.generic_endpoint_url('base-adresse', 'regions',
468
                                          slug=base_adresse.slug)
469
    mocked_get.return_value = utils.FakedResponse(
470
        content=json.dumps([FAKE_API_GEO_REGIONS[0]]), status_code=200)
471
    resp = app.get(endpoint, params={'code': '32', 'q': 'Hauts'}, status=200)
472
    assert 'nom=Hauts' in mocked_get.call_args[0][0]
473
    assert 'code=32' in mocked_get.call_args[0][0]
474

  
475
    resp = app.get(endpoint, params={'id': '32'}, status=200)
476
    assert 'code=32' in mocked_get.call_args[0][0]
261
-