From 70cbebd2505ecb55ee6f5f5e29b8aa37a1f81edb Mon Sep 17 00:00:00 2001
From: Valentin Deniaud <vdeniaud@entrouvert.com>
Date: Mon, 25 Nov 2019 18:00:33 +0100
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/, /departements/ and /regions/.
---
 .../migrations/0015_auto_20191203_1624.py     |  97 ++++++++
 passerelle/apps/base_adresse/models.py        | 229 ++++++++++++++++--
 tests/test_base_adresse.py                    | 160 +++++++++++-
 3 files changed, 465 insertions(+), 21 deletions(-)
 create mode 100644 passerelle/apps/base_adresse/migrations/0015_auto_20191203_1624.py

diff --git a/passerelle/apps/base_adresse/migrations/0015_auto_20191203_1624.py b/passerelle/apps/base_adresse/migrations/0015_auto_20191203_1624.py
new file mode 100644
index 00000000..6b24e5c9
--- /dev/null
+++ b/passerelle/apps/base_adresse/migrations/0015_auto_20191203_1624.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-12-03 15:24
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+import passerelle.apps.base_adresse.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('base_adresse', '0014_auto_20190207_0456'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='CityModel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100, verbose_name='City name')),
+                ('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='City name ascii char')),
+                ('code', models.CharField(max_length=5, verbose_name='City code')),
+                ('zipcode', models.CharField(max_length=5, verbose_name='Postal code')),
+                ('population', models.PositiveIntegerField(verbose_name='Population')),
+            ],
+            options={
+                'ordering': ['-population', 'zipcode', 'unaccent_name', 'name'],
+            },
+            bases=(passerelle.apps.base_adresse.models.UnaccentNameMixin, models.Model),
+        ),
+        migrations.CreateModel(
+            name='DepartmentModel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100, verbose_name='Department name')),
+                ('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='Department name ascii char')),
+                ('code', models.CharField(max_length=3, unique=True, verbose_name='Department code')),
+            ],
+            options={
+                'ordering': ['code'],
+            },
+            bases=(passerelle.apps.base_adresse.models.UnaccentNameMixin, models.Model),
+        ),
+        migrations.CreateModel(
+            name='RegionModel',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('name', models.CharField(max_length=100, verbose_name='Region name')),
+                ('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='Region name ascii char')),
+                ('code', models.PositiveSmallIntegerField(unique=True, verbose_name='Region code')),
+            ],
+            options={
+                'ordering': ['code'],
+            },
+            bases=(passerelle.apps.base_adresse.models.UnaccentNameMixin, models.Model),
+        ),
+        migrations.AddField(
+            model_name='baseadresse',
+            name='api_geo_url',
+            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'),
+        ),
+        migrations.AddField(
+            model_name='baseadresse',
+            name='department',
+            field=models.CharField(blank=True, max_length=3, null=True, verbose_name='Department code to get cities from'),
+        ),
+        migrations.AddField(
+            model_name='baseadresse',
+            name='region',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='Region code to get cities and departments from'),
+        ),
+        migrations.AlterField(
+            model_name='baseadresse',
+            name='zipcode',
+            field=models.CharField(blank=True, max_length=600, verbose_name='Postal codes or department number to get streets, separated with commas'),
+        ),
+        migrations.AddField(
+            model_name='departmentmodel',
+            name='region',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'),
+        ),
+        migrations.AddField(
+            model_name='citymodel',
+            name='department',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base_adresse.DepartmentModel'),
+        ),
+        migrations.AddField(
+            model_name='citymodel',
+            name='region',
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='citymodel',
+            unique_together=set([('code', 'zipcode')]),
+        ),
+    ]
diff --git a/passerelle/apps/base_adresse/models.py b/passerelle/apps/base_adresse/models.py
index 64f2767b..55d23e8a 100644
--- a/passerelle/apps/base_adresse/models.py
+++ b/passerelle/apps/base_adresse/models.py
@@ -23,16 +23,34 @@ class BaseAdresse(BaseResource):
         verbose_name=_('Service URL'),
         help_text=_('Base Adresse Web Service URL'))
 
+    api_geo_url = models.CharField(
+        max_length=128, blank=False,
+        default='https://geo.api.gouv.fr/',
+        verbose_name=_('API Geo URL'),
+        help_text=_('Base Adresse API Geo URL'))
+
     category = _('Geographic information system')
 
-    api_description = _("The API is a partial view of OpenStreetMap's Nominatim "
-                        "own API; it currently doesn't support all parameters and "
-                        "is limited to the JSON format.")
+    api_description = _("The geocoding endpoints are a partial view of OpenStreetMap's "
+                        "Nominatim own API; it currently doesn't support all parameters and "
+                        "is limited to the JSON format. The cities, departments and regions "
+                        "endpoints source data from French API Geo.")
 
     zipcode = models.CharField(
         max_length=600,
         blank=True,
-        verbose_name=_('Postal codes or county number to get streets, separated with commas'))
+        verbose_name=_('Postal codes or department number to get streets, separated with commas'))
+
+    department = models.CharField(
+        blank=True,
+        null=True,
+        max_length=3,
+        verbose_name=_('Department code to get cities from'))
+
+    region = models.PositiveSmallIntegerField(
+        blank=True,
+        null=True,
+        verbose_name=_('Region code to get cities and departments from'))
 
     class Meta:
         verbose_name = _('Base Adresse Web Service')
@@ -114,23 +132,29 @@ class BaseAdresse(BaseResource):
                     result['address']['road'] = value
         return result
 
+    @staticmethod
+    def retrieve_objects(model, id, q=None, field_lookups=()):
+        if id is not None:
+            try:
+                id = int(id)
+            except ValueError:
+                return model.objects.none()
+            return model.objects.filter(id=id)
+        qs = model.objects.all()
+        if not q:
+            return qs
+        unaccented_q = unicodedata.normalize('NFKD', q).encode('ascii', 'ignore').lower()
+        queries = [Q(**{field: unaccented_q}) for field in field_lookups]
+        return qs.filter(six.moves.reduce(Q.__or__, queries))
+
     @endpoint(description=_('Streets from zipcode'),
               parameters={
                   'zipcode': {'description': _('Zipcode'), 'example_value': '00000'}
               })
     def streets(self, request, zipcode=None, q=None, id=None, distinct=True, page_limit=None):
         result = []
-        if id is not None:
-            try:
-                id = int(id)
-            except ValueError:
-                return {'data': []}
-            streets = StreetModel.objects.filter(id=id)
-        else:
-            streets = StreetModel.objects.all()
-            if q:
-                unaccented_q = unicodedata.normalize('NFKD', q).encode('ascii', 'ignore').lower()
-                streets = streets.filter(unaccent_name__icontains=unaccented_q)
+        streets = self.retrieve_objects(StreetModel, id, q,
+                                        field_lookups=('unaccent_name__icontains',))
 
         if zipcode:
             streets = streets.filter(zipcode__startswith=zipcode)
@@ -152,6 +176,55 @@ class BaseAdresse(BaseResource):
 
         return {'data': result}
 
+    @endpoint(description=_('Cities list'),
+              parameters={
+                  'q': {'description': _("City name or zipcode"), 'example_value': 'Paris'},
+                  'code': {'description': _('City code'), 'example_value': '75056'},
+                  'region_code': {'description': _('Region code'), 'example_value': '11'},
+                  'department_code': {'description': _('Department code'), 'example_value': '75'},
+              })
+    def cities(self, request, id=None, q=None, code=None, region_code=None,
+               department_code=None):
+        cities = self.retrieve_objects(CityModel, id, q,
+                                       field_lookups=('unaccent_name__istartswith',
+                                                      'zipcode__istartswith'))
+
+        if code:
+            cities = cities.filter(code=code)
+        if region_code or self.region:
+            cities = cities.filter(region__code=region_code or self.region)
+        if department_code or self.department:
+            cities = cities.filter(department__code=department_code or self.department)
+
+        cities = cities.select_related('department', 'region')
+        return {'data': [city.to_json() for city in cities]}
+
+    @endpoint(description=_('Departments list'),
+              parameters={
+                  'q': {'description': _('Department name or code'), 'example_value': 'Nord'},
+                  'region_code': {'description': _('Region code'), 'example_value': '32'},
+              })
+    def departments(self, request, id=None, q=None, region_code=None):
+        departments = self.retrieve_objects(DepartmentModel, id, q,
+                                            field_lookups=('unaccent_name__istartswith',
+                                                           'code__istartswith'))
+
+        if region_code or self.region:
+            departments = departments.filter(region__code=region_code or self.region)
+
+        return {'data': [department.to_json() for department in departments]}
+
+    @endpoint(description=_('Regions list'),
+              parameters={
+                  'q': {'description': _('Region name or code'), 'example_value': 'Hauts-de-France'},
+              })
+    def regions(self, request, id=None, q=None, code=None):
+        regions = self.retrieve_objects(RegionModel, id, q,
+                                        field_lookups=('unaccent_name__istartswith',
+                                                       'code__istartswith'))
+
+        return {'data': [region.to_json() for region in regions]}
+
     def check_status(self):
         if self.service_url == 'https://api-adresse.data.gouv.fr/':
             result = self.search(None, '169 rue du chateau, paris')
@@ -212,18 +285,58 @@ class BaseAdresse(BaseResource):
 
         self.get_streets_queryset().filter(last_update__lt=start_update).delete()
 
+    def update_api_geo_data(self):
+        response = self.requests.get(os.path.join(self.api_geo_url, 'regions/'))
+        for data in response.json():
+            defaults = {
+                'name': data['nom'],
+            }
+            RegionModel.objects.update_or_create(code=data['code'], defaults=defaults)
+
+        response = self.requests.get(os.path.join(self.api_geo_url, 'departements/'))
+        for data in response.json():
+            defaults = {
+                'name': data['nom'],
+                'region': RegionModel.objects.get(code=data['codeRegion']),
+            }
+            DepartmentModel.objects.update_or_create(code=data['code'], defaults=defaults)
+
+        response = self.requests.get(os.path.join(self.api_geo_url, 'communes/'))
+        for data in response.json():
+            for zipcode in data['codesPostaux']:
+                defaults = {
+                    'name': data['nom'],
+                    'population': data.get('population', 0),
+                }
+                if data.get('codeDepartement'):
+                    defaults['department'] = DepartmentModel.objects.get(code=data['codeDepartement'])
+                if data.get('codeRegion'):
+                    defaults['region'] = RegionModel.objects.get(code=data['codeRegion'])
+                CityModel.objects.update_or_create(
+                    code=data['code'], zipcode=zipcode, defaults=defaults)
+
     def hourly(self):
         super(BaseAdresse, self).hourly()
+        # don't wait for daily job to grab data
         if self.get_zipcodes() and not self.get_streets_queryset().exists():
-            # don't wait for daily job to grab streets
             self.update_streets_data()
+        if not CityModel.objects.exists():
+            self.update_api_geo_data()
 
     def daily(self):
         super(BaseAdresse, self).daily()
         self.update_streets_data()
+        self.update_api_geo_data()
+
 
+class UnaccentNameMixin(object):
+
+    def save(self, *args, **kwargs):
+        self.unaccent_name = unicodedata.normalize('NFKD', self.name).encode('ascii', 'ignore').lower()
+        super(UnaccentNameMixin, self).save(*args, **kwargs)
 
-class StreetModel(models.Model):
+
+class StreetModel(UnaccentNameMixin, models.Model):
 
     city = models.CharField(_('City'), max_length=100)
     name = models.CharField(_('Street name'), max_length=150)
@@ -239,6 +352,82 @@ class StreetModel(models.Model):
     def __unicode__(self):
         return self.name
 
-    def save(self, *args, **kwargs):
-        self.unaccent_name = unicodedata.normalize('NFKD', self.name).encode('ascii', 'ignore')
-        super(StreetModel, self).save(*args, **kwargs)
+
+@six.python_2_unicode_compatible
+class RegionModel(UnaccentNameMixin, models.Model):
+
+    name = models.CharField(_('Region name'), max_length=100)
+    unaccent_name = models.CharField(_('Region name ascii char'), max_length=150, null=True)
+    code = models.PositiveSmallIntegerField(_('Region code'), unique=True)
+
+    def to_json(self):
+        return {
+            'text': str(self),
+            'id': self.id,
+            'code': self.code,
+            'name': self.name,
+        }
+
+    class Meta:
+        ordering = ['code']
+
+    def __str__(self):
+        return '%s %s' % (self.code, self.name)
+
+
+@six.python_2_unicode_compatible
+class DepartmentModel(UnaccentNameMixin, models.Model):
+
+    name = models.CharField(_('Department name'), max_length=100)
+    unaccent_name = models.CharField(_('Department name ascii char'), max_length=150, null=True)
+    code = models.CharField(_('Department code'), max_length=3, unique=True)
+    region = models.ForeignKey(RegionModel, on_delete=models.CASCADE)
+
+    def to_json(self):
+        return {
+            'text': str(self),
+            'id': self.id,
+            'code': self.code,
+            'name': self.name,
+            'region_id': self.region.id,
+        }
+
+    class Meta:
+        ordering = ['code']
+
+    def __str__(self):
+        return '%s %s' % (self.code, self.name)
+
+
+@six.python_2_unicode_compatible
+class CityModel(UnaccentNameMixin, models.Model):
+
+    name = models.CharField(_('City name'), max_length=100)
+    unaccent_name = models.CharField(_('City name ascii char'), max_length=150, null=True)
+    code = models.CharField(_('City code'), max_length=5)
+    zipcode = models.CharField(_('Postal code'), max_length=5)
+    population = models.PositiveIntegerField(_('Population'))
+    department = models.ForeignKey(DepartmentModel, on_delete=models.CASCADE, blank=True, null=True)
+    region = models.ForeignKey(RegionModel, on_delete=models.CASCADE, blank=True, null=True)
+
+    def to_json(self):
+        data = {
+            'text': str(self),
+            'id': self.id,
+            'code': self.code,
+            'name': self.name,
+            'zipcode': self.zipcode,
+            'population': self.population,
+        }
+        if self.department:
+            data['department_id'] = self.department.id
+        if self.region:
+            data['region_id'] = self.region.id
+        return data
+
+    class Meta:
+        ordering = ['-population', 'zipcode', 'unaccent_name', 'name']
+        unique_together = ('code', 'zipcode')
+
+    def __str__(self):
+        return '%s %s' % (self.zipcode, self.name)
diff --git a/tests/test_base_adresse.py b/tests/test_base_adresse.py
index acbbb94b..da4bdfb3 100644
--- a/tests/test_base_adresse.py
+++ b/tests/test_base_adresse.py
@@ -8,7 +8,8 @@ import json
 
 from django.core.management import call_command
 
-from passerelle.apps.base_adresse.models import BaseAdresse, StreetModel
+from passerelle.apps.base_adresse.models import (BaseAdresse, StreetModel, CityModel,
+                                                 DepartmentModel, RegionModel)
 
 FAKED_CONTENT = json.dumps({
     "limit": 1,
@@ -74,6 +75,35 @@ def street(db):
                                       citycode=u'73001')
 
 
+@pytest.fixture
+def region(db):
+    return RegionModel.objects.create(name=u'Auvergne-Rhône-Alpes', code=84)
+
+
+@pytest.fixture
+def department(db, region):
+    return DepartmentModel.objects.create(name=u'Savoie', code='73', region=region)
+
+
+@pytest.fixture
+def city(db, region, department):
+    return CityModel.objects.create(name=u'Chambéry', code='73065', zipcode='73000',
+                                    population=42000, region=region, department=department)
+
+
+@pytest.fixture
+def miquelon(db):
+    return CityModel.objects.create(name=u'Miquelon-Langlade', code='97501', zipcode='97500',
+                                    population=42)
+
+
+@pytest.fixture
+def mock_update_api_geo():
+    with mock.patch('passerelle.apps.base_adresse.models.BaseAdresse.update_api_geo_data',
+                    new=lambda x: None) as _fixture:
+        yield _fixture
+
+
 @mock.patch('passerelle.utils.Request.get')
 def test_base_adresse_search(mocked_get, app, base_adresse):
     endpoint = utils.generic_endpoint_url('base-adresse', 'search', slug=base_adresse.slug)
@@ -195,6 +225,7 @@ def test_base_adresse_streets_get_by_id(app, base_adresse, street):
     assert len(resp.json['data']) == 0
 
 
+@pytest.mark.usefixtures('mock_update_api_geo')
 @mock.patch('passerelle.utils.Request.get')
 def test_base_adresse_command_update(mocked_get, db, base_adresse):
     filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
@@ -214,8 +245,10 @@ def test_base_adresse_command_update(mocked_get, db, base_adresse):
     mocked_get.call_count == 2
 
 
+@pytest.mark.usefixtures('mock_update_api_geo')
 @mock.patch('passerelle.utils.Request.get')
 def test_base_adresse_command_hourly_update(mocked_get, db, base_adresse):
+    base_adresse.update_api_geo_data = lambda: None
     filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
     mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
     # check the first hourly job downloads streets
@@ -227,8 +260,10 @@ def test_base_adresse_command_hourly_update(mocked_get, db, base_adresse):
     mocked_get.call_count == 1
 
 
+@pytest.mark.usefixtures('mock_update_api_geo')
 @mock.patch('passerelle.utils.Request.get')
 def test_base_adresse_command_update_97x(mocked_get, db, base_adresse_97x):
+    base_adresse_97x.update_api_geo_data = lambda: None
     filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
     mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
     call_command('cron', 'daily')
@@ -236,8 +271,10 @@ def test_base_adresse_command_update_97x(mocked_get, db, base_adresse_97x):
     assert StreetModel.objects.count() == 2
 
 
+@pytest.mark.usefixtures('mock_update_api_geo')
 @mock.patch('passerelle.utils.Request.get')
 def test_base_adresse_command_update_corsica(mocked_get, db, base_adresse_corsica):
+    base_adresse_corsica.update_api_geo_data = lambda: None
     filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
     mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
     call_command('cron', 'daily')
@@ -247,8 +284,10 @@ def test_base_adresse_command_update_corsica(mocked_get, db, base_adresse_corsic
     assert StreetModel.objects.count() == 0
 
 
+@pytest.mark.usefixtures('mock_update_api_geo')
 @mock.patch('passerelle.utils.Request.get')
 def test_base_adresse_command_update_multiple(mocked_get, db, base_adresse_multiple):
+    base_adresse_multiple.update_api_geo_data = lambda: None
     filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
     mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
     call_command('cron', 'daily')
@@ -258,3 +297,122 @@ def test_base_adresse_command_update_multiple(mocked_get, db, base_adresse_multi
     mocked_get.assert_any_call('http://bano.openstreetmap.fr/BAN_odbl/BAN_odbl_2A-json.bz2')
     mocked_get.assert_any_call('http://bano.openstreetmap.fr/BAN_odbl/BAN_odbl_2B-json.bz2')
     assert StreetModel.objects.count() == 5
+
+
+def test_base_adresse_cities(app, base_adresse, city, department, region):
+    resp = app.get('/base-adresse/%s/cities?q=chambe' % base_adresse.slug)
+    result = resp.json['data'][0]
+    assert result['name'] == city.name
+    assert result['text'] == '%s %s' % (city.zipcode, city.name)
+    assert result['code'] == city.code
+    assert result['zipcode'] == city.zipcode
+    assert result['id'] == city.id
+    assert result['population'] == city.population
+    assert result['region_id'] == city.region.id
+    assert result['department_id'] == city.department.id
+
+    resp = app.get('/base-adresse/%s/cities?q=73' % base_adresse.slug)
+    assert resp.json['data'][0] == result
+
+    resp = app.get('/base-adresse/%s/cities?code=73065' % base_adresse.slug)
+    assert resp.json['data'][0] == result
+
+
+def test_base_adresse_cities_missing_region_and_department(app, base_adresse, miquelon):
+    resp = app.get('/base-adresse/%s/cities?q=miqu' % base_adresse.slug)
+    result = resp.json['data'][0]
+    assert result['name'] == miquelon.name
+    assert not 'department_id' in result
+    assert not 'region_id' in result
+
+
+def test_base_adresse_cities_region_department(app, base_adresse, miquelon, city):
+    reg = RegionModel.objects.create(name=u'IdF', code='11')
+    dep = DepartmentModel.objects.create(name=u'Paris', code='75', region=reg)
+    paris = CityModel.objects.create(name=u'Paris', code='75056', zipcode='75014',
+                                        population=2000000, region=reg, department=dep)
+    base_adresse.department = paris.department.code
+    base_adresse.region = paris.region.code
+    base_adresse.save()
+    resp = app.get('/base-adresse/%s/cities' % base_adresse.slug)
+    result = resp.json['data']
+    assert len(result) == 1
+    assert result[0]['name'] == paris.name
+
+    resp = app.get('/base-adresse/%s/cities?region_code=84&department_code=73' % base_adresse.slug)
+    result = resp.json['data']
+    assert len(result) == 1
+    assert result[0]['name'] == city.name
+
+
+def test_base_adresse_cities_sort_order(app, base_adresse, miquelon, city):
+    assert miquelon.population < city.population
+    resp = app.get('/base-adresse/%s/cities' % base_adresse.slug)
+    result = resp.json['data']
+    assert result[0]['name'] == city.name
+    assert result[1]['name'] == miquelon.name
+
+
+def test_base_adresse_cities_get_by_id(app, base_adresse, city):
+    for i in range(1, 10):
+        # create additional cities
+        city.pk = None
+        city.zipcode = int(city.zipcode) + i
+        city.save()
+
+    resp = app.get('/base-adresse/%s/cities?q=cham' % base_adresse.slug)
+    result = resp.json['data'][0]
+    assert len(resp.json['data']) == 10
+    city_id = result['id']
+
+    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, city_id))
+    assert len(resp.json['data']) == 1
+    result2 = resp.json['data'][0]
+    assert result2['text'] == result['text']
+
+    # non integer id.
+    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, 'XXX'))
+    assert len(resp.json['data']) == 0
+
+    # integer but without match.
+    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, '-20'))
+    assert len(resp.json['data']) == 0
+
+
+def test_base_adresse_departments(app, base_adresse, department, region):
+    resp = app.get('/base-adresse/%s/departments?q=sav' % base_adresse.slug)
+    result = resp.json['data'][0]
+    assert result['name'] == department.name
+    assert result['code'] == department.code
+    assert result['id'] == department.id
+    assert result['text'] == '%s %s' % (department.code, department.name)
+    assert result['region_id'] == region.id
+
+    resp = app.get('/base-adresse/%s/departments?q=73' % base_adresse.slug)
+    result = resp.json['data'][0]
+    assert result['name'] == department.name
+
+
+def test_base_adresse_departments_region(app, base_adresse, department):
+    reg = RegionModel.objects.create(name=u'IdF', code='11')
+    paris = DepartmentModel.objects.create(name=u'Paris', code='75', region=reg)
+    base_adresse.region = paris.region.code
+    base_adresse.save()
+    resp = app.get('/base-adresse/%s/departments' % base_adresse.slug)
+    result = resp.json['data']
+    assert len(result) == 1
+    assert result[0]['name'] == paris.name
+
+    resp = app.get('/base-adresse/%s/departments?region_code=84' % base_adresse.slug)
+    result = resp.json['data']
+    assert len(result) == 1
+    assert result[0]['name'] == department.name
+
+
+def test_base_adresse_regions(app, base_adresse, region):
+    resp = app.get('/base-adresse/%s/regions?q=au' % base_adresse.slug)
+    result = resp.json['data'][0]
+    assert result['name'] == region.name
+    assert result['code'] == region.code
+    assert result['id'] == region.id
+    assert result['text'] == '%s %s' % (region.code, region.name)
-- 
2.20.1

