Projet

Général

Profil

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

Valentin Deniaud, 06 décembre 2019 16:23

Télécharger (57,1 ko)

Voir les différences:

Subject: [PATCH 4/4] =?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_20191206_1244.py     |  95 +++++
 passerelle/apps/base_adresse/models.py        | 264 ++++++++++++-
 .../base_adresse/baseadresse_detail.html      |   5 +
 passerelle/locale/fr/LC_MESSAGES/django.po    | 354 +++++++++++++-----
 tests/test_base_adresse.py                    | 348 ++++++++++++++++-
 5 files changed, 949 insertions(+), 117 deletions(-)
 create mode 100644 passerelle/apps/base_adresse/migrations/0015_auto_20191206_1244.py
passerelle/apps/base_adresse/migrations/0015_auto_20191206_1244.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2019-12-06 11:44
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7
import passerelle.apps.base_adresse.models
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    dependencies = [
13
        ('base_adresse', '0014_auto_20190207_0456'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='CityModel',
19
            fields=[
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
                ('name', models.CharField(max_length=150, verbose_name='City name')),
22
                ('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='City name ascii char')),
23
                ('code', models.CharField(max_length=5, verbose_name='INSEE code')),
24
                ('zipcode', models.CharField(max_length=5, verbose_name='Postal code')),
25
                ('population', models.PositiveIntegerField(verbose_name='Population')),
26
                ('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')),
27
            ],
28
            options={
29
                'ordering': ['-population', 'zipcode', 'unaccent_name', 'name'],
30
            },
31
            bases=(passerelle.apps.base_adresse.models.UnaccentNameMixin, models.Model),
32
        ),
33
        migrations.CreateModel(
34
            name='DepartmentModel',
35
            fields=[
36
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
37
                ('name', models.CharField(max_length=100, verbose_name='Department name')),
38
                ('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='Department name ascii char')),
39
                ('code', models.CharField(max_length=3, unique=True, verbose_name='Department code')),
40
                ('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')),
41
            ],
42
            options={
43
                'ordering': ['code'],
44
            },
45
            bases=(passerelle.apps.base_adresse.models.UnaccentNameMixin, models.Model),
46
        ),
47
        migrations.CreateModel(
48
            name='RegionModel',
49
            fields=[
50
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
51
                ('name', models.CharField(max_length=150, verbose_name='Region name')),
52
                ('unaccent_name', models.CharField(max_length=150, null=True, verbose_name='Region name ascii char')),
53
                ('code', models.CharField(max_length=2, unique=True, verbose_name='Region code')),
54
                ('last_update', models.DateTimeField(auto_now=True, null=True, verbose_name='Last update')),
55
            ],
56
            options={
57
                'ordering': ['code'],
58
            },
59
            bases=(passerelle.apps.base_adresse.models.UnaccentNameMixin, models.Model),
60
        ),
61
        migrations.AddField(
62
            model_name='baseadresse',
63
            name='api_geo_url',
64
            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'),
65
        ),
66
        migrations.AlterField(
67
            model_name='baseadresse',
68
            name='zipcode',
69
            field=models.CharField(blank=True, max_length=600, verbose_name='Postal codes or department number to get streets, separated with commas'),
70
        ),
71
        migrations.AlterField(
72
            model_name='streetmodel',
73
            name='city',
74
            field=models.CharField(max_length=150, verbose_name='City'),
75
        ),
76
        migrations.AddField(
77
            model_name='departmentmodel',
78
            name='region',
79
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'),
80
        ),
81
        migrations.AddField(
82
            model_name='citymodel',
83
            name='department',
84
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base_adresse.DepartmentModel'),
85
        ),
86
        migrations.AddField(
87
            model_name='citymodel',
88
            name='region',
89
            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='base_adresse.RegionModel'),
90
        ),
91
        migrations.AlterUniqueTogether(
92
            name='citymodel',
93
            unique_together=set([('code', 'zipcode')]),
94
        ),
95
    ]
passerelle/apps/base_adresse/models.py
1 1
import bz2
2 2
import json
3
import os
4 3
import urlparse
5 4
import unicodedata
6 5

  
6
from requests import RequestException
7 7

  
8 8
from django.db import connection, models
9 9
from django.db.models import Q
......
15 15

  
16 16
from passerelle.base.models import BaseResource
17 17
from passerelle.utils.api import endpoint
18
from passerelle.utils.conversion import simplify
19
from passerelle.utils.jsonresponse import APIError
18 20

  
19 21

  
20 22
class BaseAdresse(BaseResource):
......
24 26
        verbose_name=_('Service URL'),
25 27
        help_text=_('Base Adresse Web Service URL'))
26 28

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

  
27 35
    category = _('Geographic information system')
28 36

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

  
33 42
    zipcode = models.CharField(
34 43
        max_length=600,
35 44
        blank=True,
36
        verbose_name=_('Postal codes or county number to get streets, separated with commas'))
45
        verbose_name=_('Postal codes or department number to get streets, separated with commas'))
37 46

  
38 47
    class Meta:
39 48
        verbose_name = _('Base Adresse Web Service')
......
135 144
        else:
136 145
            streets = StreetModel.objects.all()
137 146
            if q:
138
                unaccented_q = unicodedata.normalize('NFKD', q).encode('ascii', 'ignore').lower()
139
                streets = streets.filter(unaccent_name__icontains=unaccented_q)
147
                streets = streets.filter(unaccent_name__icontains=simplify(q))
140 148

  
141 149
        if zipcode:
142 150
            streets = streets.filter(zipcode__startswith=zipcode)
......
158 166

  
159 167
        return {'data': result}
160 168

  
169
    @endpoint(description=_('Cities list'),
170
              parameters={
171
                  'id': {'description': _('Get exactly one city using its code and postal code '
172
                                          'separated with a dot'),
173
                         'example_value': '75056.75014'},
174
                  'q': {'description': _("Search text in name or postal code"),
175
                        'example_value': 'Paris'},
176
                  'code': {'description': _('INSEE code'), 'example_value': '75056'},
177
                  'region_code': {'description': _('Region code'), 'example_value': '11'},
178
                  'department_code': {'description': _('Department code'), 'example_value': '75'},
179
              })
180
    def cities(self, request, id=None, q=None, code=None, region_code=None,
181
               department_code=None):
182
        cities = CityModel.objects.all()
183

  
184
        if id is not None:
185
            try:
186
                code, zipcode = id.split('.')
187
            except ValueError:
188
                raise APIError('Invalid id')
189
            cities = cities.filter(code=code, zipcode=zipcode)
190
        if q:
191
            unaccented_q = simplify(q)
192
            cities = cities.filter(Q(unaccent_name__istartswith=unaccented_q) |
193
                                   Q(zipcode__istartswith=unaccented_q))
194
        if code:
195
            cities = cities.filter(code=code)
196
        if region_code:
197
            cities = cities.filter(region__code=region_code)
198
        if department_code:
199
            cities = cities.filter(department__code=department_code)
200

  
201
        cities = cities.select_related('department', 'region')
202
        return {'data': [city.to_json() for city in cities]}
203

  
204
    @endpoint(description=_('Departments list'),
205
              parameters={
206
                  'id': {'description': _('Get exactly one department using its code'),
207
                         'example_value': '59'},
208
                  'q': {'description': _('Search text in name or code'), 'example_value': 'Nord'},
209
                  'region_code': {'description': _('Region code'), 'example_value': '32'},
210
              })
211
    def departments(self, request, id=None, q=None, region_code=None):
212
        departments = DepartmentModel.objects.all()
213

  
214
        if id is not None:
215
            departments = departments.filter(code=id)
216
        if q:
217
            unaccented_q = simplify(q)
218
            departments = departments.filter(Q(unaccent_name__istartswith=unaccented_q) |
219
                                             Q(code__istartswith=unaccented_q))
220
        if region_code:
221
            departments = departments.filter(region__code=region_code)
222

  
223
        departments = departments.select_related('region')
224
        return {'data': [department.to_json() for department in departments]}
225

  
226
    @endpoint(description=_('Regions list'),
227
              parameters={
228
                  'id': {'description': _('Get exactly one region using its code'),
229
                         'example_value': '32'},
230
                  'q': {'description': _('Search text in name or code'),
231
                        'example_value': 'Hauts-de-France'},
232
              })
233
    def regions(self, request, id=None, q=None):
234
        regions = RegionModel.objects.all()
235

  
236
        if id is not None:
237
            regions = regions.filter(code=id)
238
        if q:
239
            unaccented_q = simplify(q)
240
            regions = regions.filter(Q(unaccent_name__istartswith=unaccented_q) |
241
                                     Q(code__istartswith=unaccented_q))
242

  
243
        return {'data': [region.to_json() for region in regions]}
244

  
161 245
    def check_status(self):
162 246
        if self.service_url == 'https://api-adresse.data.gouv.fr/':
163 247
            result = self.search(None, '169 rue du chateau, paris')
......
174 258
            criteria |= Q(zipcode__startswith=zipcode)
175 259
        return StreetModel.objects.filter(criteria)
176 260

  
261
    def cities_exist(self):
262
        return CityModel.objects.exists()
263

  
177 264
    def update_streets_data(self):
178 265
        if not self.get_zipcodes():
179 266
            return
......
218 305

  
219 306
        self.get_streets_queryset().filter(last_update__lt=start_update).delete()
220 307

  
308
    def get_api_geo_endpoint(self, endpoint):
309
        if not self.api_geo_url:
310
            return
311
        error = None
312
        try:
313
            response = self.requests.get(urljoin(self.api_geo_url, endpoint))
314
        except RequestException as e:
315
            error = e
316
        else:
317
            if response.status_code != 200:
318
                error = 'bad status code (%s)' % response.status_code
319
            else:
320
                try:
321
                    result = response.json()
322
                except ValueError:
323
                    error = 'invalid json, got: %s' % response.text
324
        if error:
325
            self.logger.error('failed to update api geo data for endpoint %s: %s',
326
                              endpoint, error)
327
            return
328
        if not result:
329
            raise Exception('api geo returns empty json')
330
        return result
331

  
332
    def update_api_geo_data(self):
333
        regions_json = self.get_api_geo_endpoint('regions')
334
        departments_json = self.get_api_geo_endpoint('departements')
335
        cities_json = self.get_api_geo_endpoint('communes')
336
        if not (regions_json and departments_json and cities_json):
337
            return
338
        start_update = timezone.now()
339

  
340
        for data in regions_json:
341
            defaults = {
342
                'name': data['nom'],
343
            }
344
            RegionModel.objects.update_or_create(code=data['code'], defaults=defaults)
345
        RegionModel.objects.filter(last_update__lt=start_update).delete()
346

  
347
        for data in departments_json:
348
            defaults = {
349
                'name': data['nom'],
350
                'region': RegionModel.objects.get(code=data['codeRegion']),
351
            }
352
            DepartmentModel.objects.update_or_create(code=data['code'], defaults=defaults)
353
        DepartmentModel.objects.filter(last_update__lt=start_update).delete()
354

  
355
        for data in cities_json:
356
            for zipcode in data['codesPostaux']:
357
                defaults = {
358
                    'name': data['nom'],
359
                    'population': data.get('population', 0),
360
                }
361
                if data.get('codeDepartement'):
362
                    defaults['department'] = DepartmentModel.objects.get(code=data['codeDepartement'])
363
                if data.get('codeRegion'):
364
                    defaults['region'] = RegionModel.objects.get(code=data['codeRegion'])
365
                CityModel.objects.update_or_create(
366
                    code=data['code'], zipcode=zipcode, defaults=defaults)
367
        CityModel.objects.filter(last_update__lt=start_update).delete()
368

  
221 369
    def hourly(self):
222 370
        super(BaseAdresse, self).hourly()
371
        # don't wait for daily job to grab data
223 372
        if self.get_zipcodes() and not self.get_streets_queryset().exists():
224
            # don't wait for daily job to grab streets
225 373
            self.update_streets_data()
374
        if not CityModel.objects.exists():
375
            self.update_api_geo_data()
226 376

  
227 377
    def daily(self):
228 378
        super(BaseAdresse, self).daily()
229 379
        self.update_streets_data()
380
        self.update_api_geo_data()
230 381

  
231 382

  
232
class StreetModel(models.Model):
383
class UnaccentNameMixin(object):
233 384

  
234
    city = models.CharField(_('City'), max_length=100)
385
    def save(self, *args, **kwargs):
386
        self.unaccent_name = unicodedata.normalize('NFKD', self.name).encode('ascii', 'ignore').lower()
387
        super(UnaccentNameMixin, self).save(*args, **kwargs)
388

  
389

  
390
class StreetModel(UnaccentNameMixin, models.Model):
391

  
392
    city = models.CharField(_('City'), max_length=150)
235 393
    name = models.CharField(_('Street name'), max_length=150)
236 394
    unaccent_name = models.CharField(_('Street name ascii char'), max_length=150, null=True)
237 395
    zipcode = models.CharField(_('Postal code'), max_length=5)
......
245 403
    def __unicode__(self):
246 404
        return self.name
247 405

  
248
    def save(self, *args, **kwargs):
249
        self.unaccent_name = unicodedata.normalize('NFKD', self.name).encode('ascii', 'ignore')
250
        super(StreetModel, self).save(*args, **kwargs)
406

  
407
@six.python_2_unicode_compatible
408
class RegionModel(UnaccentNameMixin, models.Model):
409

  
410
    name = models.CharField(_('Region name'), max_length=150)
411
    unaccent_name = models.CharField(_('Region name ascii char'), max_length=150, null=True)
412
    code = models.CharField(_('Region code'), max_length=2, unique=True)
413
    last_update = models.DateTimeField(_('Last update'), null=True, auto_now=True)
414

  
415
    def to_json(self):
416
        return {
417
            'text': str(self),
418
            'id': self.code,
419
            'code': self.code,
420
            'name': self.name,
421
        }
422

  
423
    class Meta:
424
        ordering = ['code']
425

  
426
    def __str__(self):
427
        return '%s %s' % (self.code, self.name)
428

  
429

  
430
@six.python_2_unicode_compatible
431
class DepartmentModel(UnaccentNameMixin, models.Model):
432

  
433
    name = models.CharField(_('Department name'), max_length=100)
434
    unaccent_name = models.CharField(_('Department name ascii char'), max_length=150, null=True)
435
    code = models.CharField(_('Department code'), max_length=3, unique=True)
436
    region = models.ForeignKey(RegionModel, on_delete=models.CASCADE)
437
    last_update = models.DateTimeField(_('Last update'), null=True, auto_now=True)
438

  
439
    def to_json(self):
440
        return {
441
            'text': str(self),
442
            'id': self.code,
443
            'code': self.code,
444
            'name': self.name,
445
            'region_code': self.region.code,
446
            'region_name': self.region.name,
447
        }
448

  
449
    class Meta:
450
        ordering = ['code']
451

  
452
    def __str__(self):
453
        return '%s %s' % (self.code, self.name)
454

  
455

  
456
@six.python_2_unicode_compatible
457
class CityModel(UnaccentNameMixin, models.Model):
458

  
459
    name = models.CharField(_('City name'), max_length=150)
460
    unaccent_name = models.CharField(_('City name ascii char'), max_length=150, null=True)
461
    code = models.CharField(_('INSEE code'), max_length=5)
462
    zipcode = models.CharField(_('Postal code'), max_length=5)
463
    population = models.PositiveIntegerField(_('Population'))
464
    department = models.ForeignKey(DepartmentModel, on_delete=models.CASCADE, blank=True, null=True)
465
    region = models.ForeignKey(RegionModel, on_delete=models.CASCADE, blank=True, null=True)
466
    last_update = models.DateTimeField(_('Last update'), null=True, auto_now=True)
467

  
468
    def to_json(self):
469
        data = {
470
            'text': str(self),
471
            'id': '%s.%s' % (self.code, self.zipcode),
472
            'code': self.code,
473
            'name': self.name,
474
            'zipcode': self.zipcode,
475
            'population': self.population,
476
            'department_code': self.department.code if self.department else None,
477
            'department_name': self.department.name if self.department else None,
478
            'region_code': self.region.code if self.region else None,
479
            'region_name': self.region.name if self.region else None,
480
        }
481
        return data
482

  
483
    class Meta:
484
        ordering = ['-population', 'zipcode', 'unaccent_name', 'name']
485
        unique_together = ('code', 'zipcode')
486

  
487
    def __str__(self):
488
        return '%s %s' % (self.zipcode, self.name)
passerelle/apps/base_adresse/templates/base_adresse/baseadresse_detail.html
8 8
{% trans "Street data is not available yet, it should soon be downloaded." %}
9 9
</div>
10 10
{% endif %}
11
{% if object.api_geo_url and not object.cities_exist %}
12
<div class="infonotice">
13
{% trans "API Géo data is not available yet, it should soon be downloaded." %}
14
</div>
15
{% endif %}
11 16
{% endblock %}
12 17

  
13 18
{% block security %}
passerelle/locale/fr/LC_MESSAGES/django.po
7 7
msgstr ""
8 8
"Project-Id-Version: Passerelle 0\n"
9 9
"Report-Msgid-Bugs-To: \n"
10
"POT-Creation-Date: 2019-11-25 14:55+0100\n"
10
"POT-Creation-Date: 2019-12-06 12:34+0100\n"
11 11
"PO-Revision-Date: 2019-11-07 19:50+0100\n"
12 12
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
13 13
"Language: fr\n"
......
87 87
#: contrib/maarch/models.py:37 contrib/mdph13/models.py:56
88 88
#: contrib/planitech/models.py:277 contrib/seisin_by_email/models.py:37
89 89
#: contrib/solis_apa/models.py:53 contrib/teamnet_axel/models.py:77
90
#: contrib/toulouse_axel/models.py:118
90
#: contrib/toulouse_axel/models.py:122
91 91
msgid "Business Process Connectors"
92 92
msgstr "Connecteurs métiers"
93 93

  
......
225 225
msgid "API Particulier"
226 226
msgstr "API Particulier"
227 227

  
228
#: apps/arcgis/models.py:33 apps/base_adresse/models.py:26
228
#: apps/arcgis/models.py:33 apps/base_adresse/models.py:36
229 229
#: apps/opengis/models.py:57 contrib/adict/models.py:25
230 230
#: contrib/grandlyon_streetsections/models.py:43
231 231
msgid "Geographic information system"
......
257 257
msgstr "Nom de la couche ou de la table"
258 258

  
259 259
#: apps/arcgis/models.py:56 apps/arcgis/models.py:170
260
#: apps/opengis/models.py:217 contrib/adict/models.py:36
260
#: apps/base_adresse/models.py:91 apps/opengis/models.py:217
261
#: contrib/adict/models.py:36
261 262
msgid "Latitude"
262 263
msgstr "Latitude"
263 264

  
264 265
#: apps/arcgis/models.py:57 apps/arcgis/models.py:171
265
#: apps/opengis/models.py:218 contrib/adict/models.py:37
266
#: apps/base_adresse/models.py:92 apps/opengis/models.py:218
267
#: contrib/adict/models.py:37
266 268
msgid "Longitude"
267 269
msgstr "Longitude"
268 270

  
......
365 367
msgid "Association ID"
366 368
msgstr "Identifiant de l'association"
367 369

  
368
#: apps/astregs/models.py:352 contrib/toulouse_axel/models.py:135
369
#: contrib/toulouse_axel/models.py:169
370
#: apps/astregs/models.py:352 contrib/toulouse_axel/models.py:139
371
#: contrib/toulouse_axel/models.py:177 contrib/toulouse_axel/models.py:205
372
#: contrib/toulouse_axel/models.py:215
370 373
msgid "Publik ID"
371 374
msgstr "Publik NameID"
372 375

  
......
545 548
msgid "Anything"
546 549
msgstr "Informations additionnelles"
547 550

  
548
#: apps/base_adresse/models.py:23 apps/bdp/models.py:13 apps/gdc/models.py:25
551
#: apps/base_adresse/models.py:27 apps/bdp/models.py:13 apps/gdc/models.py:25
549 552
#: apps/okina/models.py:29 apps/solis/models.py:78 contrib/gdema/models.py:147
550 553
msgid "Service URL"
551 554
msgstr "URL du webservice"
552 555

  
553
#: apps/base_adresse/models.py:24
556
#: apps/base_adresse/models.py:28
554 557
msgid "Base Adresse Web Service URL"
555 558
msgstr "URL du webservice de la Base Adresse"
556 559

  
557
#: apps/base_adresse/models.py:31
558
msgid "Postal codes or county number to get streets, separated with commas"
560
#: apps/base_adresse/models.py:33
561
msgid "API Geo URL"
562
msgstr "URL de base de l'API Géo"
563

  
564
#: apps/base_adresse/models.py:34
565
msgid "Base Adresse API Geo URL"
566
msgstr "URL du webservice de l'API Géo"
567

  
568
#: apps/base_adresse/models.py:38
569
msgid ""
570
"The geocoding endpoints are a partial view of OpenStreetMap's Nominatim own "
571
"API; it currently doesn't support all parameters and is limited to the JSON "
572
"format. The cities, departments and regions endpoints source data from "
573
"French API Geo."
574
msgstr ""
575
"\n"
576
"L'API est une vue partielle de l'API Nominatim de OpenStreetMap ;\n"
577
" elle ne prend pour le moment pas en charge tous les paramètres et est "
578
"limitée au format JSON. Les points d'accès cities, departments et regions "
579
"utilisent les données de l'API Géo.\n"
580

  
581
#: apps/base_adresse/models.py:46
582
msgid "Postal codes or department number to get streets, separated with commas"
559 583
msgstr ""
560 584
"Codes postaux ou numéros de départements d'où récupérer les rues, séparés "
561 585
"par des virgules."
562 586

  
563
#: apps/base_adresse/models.py:34
587
#: apps/base_adresse/models.py:49
564 588
msgid "Base Adresse Web Service"
565 589
msgstr "Service Web Base Adresse"
566 590

  
567
#: apps/base_adresse/models.py:213 apps/family/models.py:408
568
#: apps/family/models.py:449 contrib/iws/models.py:155
569
msgid "City"
570
msgstr "Ville"
591
#: apps/base_adresse/models.py:52
592
msgid "Geocoding"
593
msgstr "Géocodage"
594

  
595
#: apps/base_adresse/models.py:54
596
#: contrib/agoraplus/templates/passerelle/contrib/agoraplus/detail.html:44
597
msgid "Address"
598
msgstr "Adresse"
599

  
600
#: apps/base_adresse/models.py:89
601
msgid "Reverse geocoding"
602
msgstr "Géocodage inversé"
603

  
604
#: apps/base_adresse/models.py:128
605
msgid "Streets from zipcode"
606
msgstr "Rues d'un code postal :"
607

  
608
#: apps/base_adresse/models.py:130
609
#, fuzzy
610
#| msgid "Get exactly one region using its code"
611
msgid "Get exactly one street"
612
msgstr "Récupérer exactement une région depuis son code"
571 613

  
572
#: apps/base_adresse/models.py:214 apps/family/models.py:404
573
#: apps/family/models.py:445
614
#: apps/base_adresse/models.py:131 apps/base_adresse/models.py:392
615
#: apps/family/models.py:404 apps/family/models.py:445
574 616
msgid "Street name"
575 617
msgstr "Nom de la voie"
576 618

  
577
#: apps/base_adresse/models.py:215
619
#: apps/base_adresse/models.py:132 apps/family/models.py:407
620
#: apps/family/models.py:448
621
msgid "Zipcode"
622
msgstr "Code postal"
623

  
624
#: apps/base_adresse/models.py:133
625
msgid "Maximum number of results to return"
626
msgstr "Nombre maximal de résultats"
627

  
628
#: apps/base_adresse/models.py:135
629
msgid "Remove duplicate streets"
630
msgstr "Ne pas inclure les doublons"
631

  
632
#: apps/base_adresse/models.py:170
633
msgid "Cities list"
634
msgstr "Liste des villes"
635

  
636
#: apps/base_adresse/models.py:172
637
msgid ""
638
"Get exactly one city using its code and postal code separated with a dot"
639
msgstr ""
640
"Récupérer exactement une ville grâce à son code INSEE et son code postal, "
641
"séparés par un point"
642

  
643
#: apps/base_adresse/models.py:175
644
msgid "Search text in name or postal code"
645
msgstr "Recherche du texte dans le nom et le code postal"
646

  
647
#: apps/base_adresse/models.py:177 apps/base_adresse/models.py:459
648
msgid "INSEE code"
649
msgstr ""
650

  
651
#: apps/base_adresse/models.py:178 apps/base_adresse/models.py:210
652
#: apps/base_adresse/models.py:411
653
msgid "Region code"
654
msgstr "Code de la région"
655

  
656
#: apps/base_adresse/models.py:179 apps/base_adresse/models.py:434
657
msgid "Department code"
658
msgstr "Code du département"
659

  
660
#: apps/base_adresse/models.py:205
661
msgid "Departments list"
662
msgstr "Liste des départements"
663

  
664
#: apps/base_adresse/models.py:207
665
msgid "Get exactly one department using its code"
666
msgstr ""
667

  
668
#: apps/base_adresse/models.py:209 apps/base_adresse/models.py:231
669
msgid "Search text in name or code"
670
msgstr "Recherche du texte dans le nom ou le code"
671

  
672
#: apps/base_adresse/models.py:227
673
msgid "Regions list"
674
msgstr "Liste des régions"
675

  
676
#: apps/base_adresse/models.py:229
677
msgid "Get exactly one region using its code"
678
msgstr "Récupérer exactement une région depuis son code"
679

  
680
#: apps/base_adresse/models.py:391 apps/family/models.py:408
681
#: apps/family/models.py:449 contrib/iws/models.py:155
682
msgid "City"
683
msgstr "Ville"
684

  
685
#: apps/base_adresse/models.py:393
578 686
msgid "Street name ascii char"
579 687
msgstr "Nom de la voie (en ASCII)"
580 688

  
581
#: apps/base_adresse/models.py:216
689
#: apps/base_adresse/models.py:394 apps/base_adresse/models.py:460
582 690
msgid "Postal code"
583 691
msgstr "Code postal"
584 692

  
585
#: apps/base_adresse/models.py:217
693
#: apps/base_adresse/models.py:395
586 694
msgid "Street type"
587 695
msgstr "Type de la voie"
588 696

  
589
#: apps/base_adresse/models.py:218
697
#: apps/base_adresse/models.py:396
590 698
msgid "City Code"
591 699
msgstr "Code de la ville (INSEE)"
592 700

  
593
#: apps/base_adresse/models.py:219 contrib/tcl/models.py:175
594
#: contrib/tcl/models.py:262
701
#: apps/base_adresse/models.py:397 apps/base_adresse/models.py:412
702
#: apps/base_adresse/models.py:436 apps/base_adresse/models.py:464
703
#: contrib/tcl/models.py:175 contrib/tcl/models.py:262
595 704
msgid "Last update"
596 705
msgstr "Dernière mise à jour"
597 706

  
707
#: apps/base_adresse/models.py:409
708
msgid "Region name"
709
msgstr "Nom de la région"
710

  
711
#: apps/base_adresse/models.py:410
712
msgid "Region name ascii char"
713
msgstr "Nom de la région (en ASCII)"
714

  
715
#: apps/base_adresse/models.py:432
716
msgid "Department name"
717
msgstr "Nom du département"
718

  
719
#: apps/base_adresse/models.py:433
720
msgid "Department name ascii char"
721
msgstr "Nom du département (en ASCII)"
722

  
723
#: apps/base_adresse/models.py:457
724
msgid "City name"
725
msgstr "Nom de la ville"
726

  
727
#: apps/base_adresse/models.py:458
728
msgid "City name ascii char"
729
msgstr "Nom de la ville (en ASCII)"
730

  
731
#: apps/base_adresse/models.py:461
732
msgid "Population"
733
msgstr "Population"
734

  
598 735
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:8
599 736
msgid "Street data is not available yet, it should soon be downloaded."
600 737
msgstr ""
601 738
"Les données des rues ne sont pas encore disponibles; elles devraient bientôt "
602 739
"être téléchargées."
603 740

  
604
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:15
605
msgid ""
606
"\n"
607
"The API is a partial view of <a href=\"http://wiki.openstreetmap.org/wiki/"
608
"Nominatim\">Nominatim</a>\n"
609
"own API; it currently doesn't support all parameters and is limited to the "
610
"JSON\n"
611
"format.\n"
741
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:13
742
#, fuzzy
743
#| msgid "Street data is not available yet, it should soon be downloaded."
744
msgid "API Géo data is not available yet, it should soon be downloaded."
612 745
msgstr ""
613
"\n"
614
"L'API est une vue partielle de l'API de <a href=\"http://wiki.openstreetmap."
615
"org/wiki/Nominatim\">Nominatim</a>;\n"
616
" elle ne prend pour le moment pas en charge tous les paramètres et est "
617
"limitée au format JSON.\n"
618

  
619
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:23
620
msgid "Geocoding:"
621
msgstr "Géocodage :"
622

  
623
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:25
624
msgid "Reverse geocoding:"
625
msgstr "Géocodage inversé :"
626

  
627
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:27
628
msgid "Streets from zipcode:"
629
msgstr "Rues d'un code postal :"
746
"Les données des rues ne sont pas encore disponibles; elles devraient bientôt "
747
"être téléchargées."
630 748

  
631
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:34
749
#: apps/base_adresse/templates/base_adresse/baseadresse_detail.html:20
632 750
msgid "Accessing the listings is open."
633 751
msgstr "L'accès aux listes est ouvert."
634 752

  
......
1182 1300
msgstr "Nom de la feuille"
1183 1301

  
1184 1302
#: apps/csvdatasource/models.py:136 apps/feeds/models.py:29
1185
#: apps/jsondatastore/models.py:78
1303
#: apps/jsondatastore/models.py:79
1186 1304
#: apps/mdel/templates/mdel/mdel_detail.html:124 contrib/nancypoll/models.py:21
1187 1305
msgid "Data Sources"
1188 1306
msgstr "Sources des données"
......
1245 1363
msgstr "Êtes-vous sûr·e de vouloir supprimer cette requête ?"
1246 1364

  
1247 1365
#: apps/csvdatasource/templates/csvdatasource/query_confirm_delete.html:20
1248
#: apps/jsondatastore/models.py:142
1366
#: apps/jsondatastore/models.py:160
1249 1367
#: apps/sp_fr/templates/sp_fr/resource_detail.html:32
1250 1368
#: templates/passerelle/manage/accessright_confirm_delete.html:12
1251 1369
#: templates/passerelle/manage/apiuser_confirm_delete.html:20
......
1348 1466
msgid "Address complement"
1349 1467
msgstr "Complément d'adresse"
1350 1468

  
1351
#: apps/family/models.py:407 apps/family/models.py:448
1352
msgid "Zipcode"
1353
msgstr "Code postal"
1354

  
1355 1469
#: apps/family/models.py:409
1356 1470
msgid "Family quotient"
1357 1471
msgstr "Quotient familial"
......
1570 1684
msgid "Form identifier"
1571 1685
msgstr "Identifiant de la demande"
1572 1686

  
1573
#: apps/jsondatastore/models.py:47
1687
#: apps/jsondatastore/models.py:48
1574 1688
msgid "uuid"
1575 1689
msgstr "uuid"
1576 1690

  
1577
#: apps/jsondatastore/models.py:50
1691
#: apps/jsondatastore/models.py:51
1578 1692
msgid "Content"
1579 1693
msgstr "Contenu"
1580 1694

  
1581
#: apps/jsondatastore/models.py:80
1695
#: apps/jsondatastore/models.py:81
1582 1696
msgid "Template for \"text\" key value"
1583 1697
msgstr "Modèle à appliquer pour la valeur de la clé « text »"
1584 1698

  
1585
#: apps/jsondatastore/models.py:84
1699
#: apps/jsondatastore/models.py:85
1586 1700
msgid "JSON Data Store"
1587 1701
msgstr "Stockage de données JSON"
1588 1702

  
1589
#: apps/jsondatastore/models.py:87
1703
#: apps/jsondatastore/models.py:88
1590 1704
msgid "Listing"
1591 1705
msgstr "Liste"
1592 1706

  
1593
#: apps/jsondatastore/models.py:99
1707
#: apps/jsondatastore/models.py:90
1708
msgid ""
1709
"More filtering on attributes is possible using \"key=val\" additionals "
1710
"parameters"
1711
msgstr ""
1712

  
1713
#: apps/jsondatastore/models.py:94 apps/jsondatastore/models.py:145
1714
#: apps/jsondatastore/models.py:163
1715
msgid "Object identifier"
1716
msgstr "Identifiant de l'objet"
1717

  
1718
#: apps/jsondatastore/models.py:98
1719
#, fuzzy
1720
#| msgid "Template for \"text\" key value"
1721
msgid "Filter on \"text\" key value"
1722
msgstr "Modèle à appliquer pour la valeur de la clé « text »"
1723

  
1724
#: apps/jsondatastore/models.py:117
1594 1725
msgid "Create"
1595 1726
msgstr "Créer"
1596 1727

  
1597
#: apps/jsondatastore/models.py:124
1728
#: apps/jsondatastore/models.py:142
1598 1729
msgid "Get"
1599 1730
msgstr "Récupérer"
1600 1731

  
1601
#: apps/jsondatastore/models.py:125
1732
#: apps/jsondatastore/models.py:143
1602 1733
msgid "Replace"
1603 1734
msgstr "Remplacer"
1604 1735

  
1605
#: apps/jsondatastore/models.py:126
1736
#: apps/jsondatastore/models.py:144
1606 1737
msgid "Update"
1607 1738
msgstr "Mise à jour"
1608 1739

  
1609
#: apps/jsondatastore/models.py:127 apps/jsondatastore/models.py:145
1610
msgid "Object identifier"
1611
msgstr "Identifiant de l'objet"
1612

  
1613
#: apps/jsondatastore/models.py:155
1740
#: apps/jsondatastore/models.py:173
1614 1741
msgid "Get a single object by attribute"
1615 1742
msgstr "Récupérer un objet selon un attribut"
1616 1743

  
1617
#: apps/jsondatastore/models.py:156
1744
#: apps/jsondatastore/models.py:174
1618 1745
msgid "Attribute name"
1619 1746
msgstr "Nom de l'attribut"
1620 1747

  
1621
#: apps/jsondatastore/models.py:157
1748
#: apps/jsondatastore/models.py:175
1622 1749
msgid "Attribute value"
1623 1750
msgstr "Valeur de l'attribut"
1624 1751

  
......
2078 2205
msgstr "Liste des usagers Solis APA liés au name_id"
2079 2206

  
2080 2207
#: apps/solis/models.py:310 apps/solis/models.py:322 apps/solis/models.py:367
2081
#: apps/solis/models.py:575 apps/solis/models.py:587
2208
#: apps/solis/models.py:580 apps/solis/models.py:592
2082 2209
msgid "user identifier"
2083 2210
msgstr "identifiant de l'utilisateur"
2084 2211

  
......
2112 2239
msgid "Send data to \"integrationDemandeApa\""
2113 2240
msgstr "Créer une demande APA — envoi vers « integrationDemandeApa »"
2114 2241

  
2115
#: apps/solis/models.py:528
2242
#: apps/solis/models.py:533
2116 2243
msgid ""
2117 2244
"Create link between name_id and Solis RSA. Payload: name_id, user_id, code"
2118 2245
msgstr ""
2119 2246
"Créer un lien entre le name_id et un usager Solis RSA (paramètres dans un "
2120 2247
"dictionnaire : name_id, user_id et code)"
2121 2248

  
2122
#: apps/solis/models.py:555
2249
#: apps/solis/models.py:560
2123 2250
msgid "Delete a Solis RSA link. Payload: name_id, user_id"
2124 2251
msgstr ""
2125 2252
"Supprimer le lien entre l'usager Solis RSA et le name_id (paramètres dans un "
2126 2253
"dictionnaire : name_id et user_id)"
2127 2254

  
2128
#: apps/solis/models.py:572
2255
#: apps/solis/models.py:577
2129 2256
msgid "List linked Solis RSA users"
2130 2257
msgstr "Liste des usagers Solis RSA liés au name_id"
2131 2258

  
2132
#: apps/solis/models.py:584
2259
#: apps/solis/models.py:589
2133 2260
msgid "Get informations about a linked Solis RSA user"
2134 2261
msgstr "Renvoie les informations d'un usager Solis RSA lié"
2135 2262

  
2136
#: apps/solis/models.py:591
2263
#: apps/solis/models.py:596
2137 2264
msgid "Solis RSA user identifier"
2138 2265
msgstr "identifiant de l'usager Solis RSA"
2139 2266

  
2140
#: apps/solis/models.py:595
2267
#: apps/solis/models.py:600
2141 2268
msgid ""
2142 2269
"individu, actions, allocataires, engagements, evaluations, evenements, "
2143 2270
"indus, menages, presences, rdvs"
......
2145 2272
"individu, actions, allocataires, engagements, evaluations, evenements, "
2146 2273
"indus, menages, presences, rdvs"
2147 2274

  
2148
#: apps/solis/models.py:600
2275
#: apps/solis/models.py:605
2149 2276
msgid "get a specific item, if applicable"
2150 2277
msgstr "renvoie un élément spécifique, si possible"
2151 2278

  
2152
#: apps/solis/models.py:603
2279
#: apps/solis/models.py:608
2153 2280
msgid "get linked informations (comma separated list, empty for all)"
2154 2281
msgstr ""
2155 2282
"renvoie aussi les informations liées (noms des «_links» séparés par une "
2156 2283
"virgule, ou vide pour tout récupérer)"
2157 2284

  
2158
#: apps/solis/models.py:607
2285
#: apps/solis/models.py:612
2159 2286
msgid ""
2160 2287
"filter response (list), ex: idStructure=399 or idStructure!=399,"
2161 2288
"prescriptionPlacement=Placement"
......
2483 2610
msgid "Down"
2484 2611
msgstr "Hors service"
2485 2612

  
2486
#: base/models.py:865
2613
#: base/models.py:869
2487 2614
msgid "Basic authentication username"
2488 2615
msgstr "Identifiant d'authentification basique"
2489 2616

  
2490
#: base/models.py:869
2617
#: base/models.py:873
2491 2618
msgid "Basic authentication password"
2492 2619
msgstr "Mot de passe pour l'authentification basique"
2493 2620

  
2494
#: base/models.py:872
2621
#: base/models.py:876
2495 2622
msgid "TLS client certificate"
2496 2623
msgstr "Certificat client"
2497 2624

  
2498
#: base/models.py:876
2625
#: base/models.py:880
2499 2626
msgid "TLS trusted CAs"
2500 2627
msgstr "Autorités de confiance"
2501 2628

  
2502
#: base/models.py:880
2629
#: base/models.py:884
2503 2630
msgid "TLS verify certificates"
2504 2631
msgstr "Vérification du certificat client"
2505 2632

  
2506
#: base/models.py:885
2633
#: base/models.py:889
2507 2634
msgid "HTTP and HTTPS proxy"
2508 2635
msgstr "Proxy HTTP et HTTPS"
2509 2636

  
......
2648 2775
msgid "Update profession:"
2649 2776
msgstr "Mettre à jour la profession :"
2650 2777

  
2651
#: contrib/agoraplus/templates/passerelle/contrib/agoraplus/detail.html:44
2652
msgid "Address"
2653
msgstr "Adresse"
2654

  
2655 2778
#: contrib/agoraplus/templates/passerelle/contrib/agoraplus/detail.html:46
2656 2779
#: contrib/solis_apa/templates/passerelle/contrib/solis_apa/detail.html:18
2657 2780
msgid "Communes:"
......
3144 3267
#: contrib/greco/models.py:78 contrib/iparapheur/models.py:86
3145 3268
#: contrib/iparapheur/models.py:87 contrib/maarch/models.py:25
3146 3269
#: contrib/seisin_by_email/models.py:25 contrib/teamnet_axel/models.py:59
3147
#: contrib/toulouse_axel/models.py:115
3270
#: contrib/toulouse_axel/models.py:119
3148 3271
msgid "WSDL URL"
3149 3272
msgstr "URL du WSDL"
3150 3273

  
......
3765 3888
msgid "Get invoice details"
3766 3889
msgstr "Récupérer les détails d'une facture"
3767 3890

  
3768
#: contrib/stub_invoices/models.py:82 contrib/stub_invoices/models.py:93
3769
#: contrib/stub_invoices/models.py:107
3891
#: contrib/stub_invoices/models.py:82 contrib/stub_invoices/models.py:94
3892
#: contrib/stub_invoices/models.py:109
3770 3893
msgid "Invoice identifier"
3771 3894
msgstr "Identifiant de facture"
3772 3895

  
......
3774 3897
msgid "Get invoice as a PDF file"
3775 3898
msgstr "Récupérer une facture au format PDF"
3776 3899

  
3777
#: contrib/stub_invoices/models.py:103
3900
#: contrib/stub_invoices/models.py:90 contrib/stub_invoices/models.py:105
3901
msgid "not yet implemented"
3902
msgstr ""
3903

  
3904
#: contrib/stub_invoices/models.py:104
3778 3905
msgid "Pay invoice"
3779 3906
msgstr "Payer une facture"
3780 3907

  
......
3864 3991
msgid "Call stopped:"
3865 3992
msgstr "Fin d'appel"
3866 3993

  
3867
#: contrib/toulouse_axel/models.py:116
3994
#: contrib/toulouse_axel/models.py:120
3868 3995
msgid "Toulouse Axel WSDL URL"
3869 3996
msgstr "URL du WSDL Axel"
3870 3997

  
3871
#: contrib/toulouse_axel/models.py:121
3998
#: contrib/toulouse_axel/models.py:125
3872 3999
msgid "Toulouse Axel"
3873 4000
msgstr "Toulouse Axel"
3874 4001

  
3875
#: contrib/toulouse_axel/models.py:132
4002
#: contrib/toulouse_axel/models.py:136
3876 4003
msgid "Create link between user and Toulouse Axel"
3877 4004
msgstr "Lier un compte usager à Toulouse Axel"
3878 4005

  
3879
#: contrib/toulouse_axel/models.py:166
4006
#: contrib/toulouse_axel/models.py:173
4007
#, fuzzy
4008
#| msgid "Create link between user and Toulouse Axel"
4009
msgid "Delete link between user and Toulouse Axel"
4010
msgstr "Lier un compte usager à Toulouse Axel"
4011

  
4012
#: contrib/toulouse_axel/models.py:202
3880 4013
msgid "Get information about user's family"
3881 4014
msgstr "Renvoie les informations famille d'un usager"
3882 4015

  
4016
#: contrib/toulouse_axel/models.py:212
4017
#, fuzzy
4018
#| msgid "Get information about user's family"
4019
msgid "Get information about a child"
4020
msgstr "Renvoie les informations famille d'un usager"
4021

  
4022
#: contrib/toulouse_axel/models.py:216
4023
#, fuzzy
4024
#| msgid "Child:"
4025
msgid "Child ID"
4026
msgstr "Enfant :"
4027

  
3883 4028
#: pbx/models.py:22
3884 4029
msgid "welco URL"
3885 4030
msgstr "URL de Welco"
......
3893 4038
msgstr "L'envoi de messages est limité aux utilisateurs d'API suivants :"
3894 4039

  
3895 4040
#: templates/passerelle/base.html:16 templates/passerelle/manage.html:5
3896
#: views.py:89
4041
#: views.py:90
3897 4042
msgid "Web Services"
3898 4043
msgstr "Services web"
3899 4044

  
......
4123 4268
msgid "Incorrect password. Try again."
4124 4269
msgstr "Mot de passe incorrect ; essayez à nouveau."
4125 4270

  
4126
#: utils/sftp.py:191
4271
#: utils/sftp.py:193
4127 4272
msgid "SSH private key needs a password"
4128 4273
msgstr "La clé privée SSH nécessite un mot de passe"
4129 4274

  
4130
#: utils/sftp.py:193
4275
#: utils/sftp.py:195
4131 4276
msgid "SSH private key invalid"
4132 4277
msgstr "Clé privée SSH invalide"
4133 4278

  
4279
#~ msgid "City code"
4280
#~ msgstr "Code de la ville (INSEE)"
4281

  
4134 4282
#~ msgid "Same filter, but case insensitive:"
4135 4283
#~ msgstr "Même filtre mais insensible à la casse :"
4136 4284

  
tests/test_base_adresse.py
6 6
import utils
7 7
import json
8 8

  
9
from requests.exceptions import ConnectionError
10

  
9 11
from django.core.management import call_command
12
from django.core.management.base import CommandError
13
from django.utils.six.moves.urllib.parse import urljoin
10 14

  
11
from passerelle.apps.base_adresse.models import BaseAdresse, StreetModel
15
from passerelle.apps.base_adresse.models import (BaseAdresse, StreetModel, CityModel,
16
                                                 DepartmentModel, RegionModel)
12 17

  
13 18
FAKED_CONTENT = json.dumps({
14 19
    "limit": 1,
......
41 46

  
42 47
FAKE_DATA = ''
43 48

  
49
FAKE_API_GEO_LIST = [
50
    {
51
        "code": "75056",
52
        "codeDepartement": "75",
53
        "codeRegion": "11",
54
        "codesPostaux": [
55
            "75001",
56
            "75002",
57
        ],
58
        "nom": "Paris",
59
        "population": 2190327,
60
    },
61
    {
62
        "code": "97501",
63
        "codesPostaux": [
64
            "97500"
65
        ],
66
        "nom": "Miquelon-Langlade",
67
        "population": 596
68
    }
69
]
70

  
71
FAKE_API_GEO = json.dumps(FAKE_API_GEO_LIST)
72

  
73
FAKE_API_GEO_DEPARTMENTS = json.dumps([
74
    {
75
        "code": "75",
76
        "codeRegion": "11",
77
        "nom": "Paris"
78
    },
79
    {
80
        "code": "58",
81
        "codeRegion": "27",
82
        "nom": "Nièvre",
83
    }
84
])
85

  
86
FAKE_API_GEO_REGIONS = json.dumps([
87
    {
88
        "code": "11",
89
        "nom": "Île-de-France"
90
    },
91
    {
92
        "code": "27",
93
        "nom": "Bourgogne-Franche-Comté"
94
    }
95
])
96

  
44 97

  
45 98
@pytest.fixture
46 99
def base_adresse(db):
......
74 127
                                      citycode=u'73001')
75 128

  
76 129

  
130
@pytest.fixture
131
def region(db):
132
    return RegionModel.objects.create(name=u'Auvergne-Rhône-Alpes', code='84')
133

  
134

  
135
@pytest.fixture
136
def department(db, region):
137
    return DepartmentModel.objects.create(name=u'Savoie', code='73', region=region)
138

  
139

  
140
@pytest.fixture
141
def city(db, region, department):
142
    return CityModel.objects.create(name=u'Chambéry', code='73065', zipcode='73000',
143
                                    population=42000, region=region, department=department)
144

  
145

  
146
@pytest.fixture
147
def miquelon(db):
148
    return CityModel.objects.create(name=u'Miquelon-Langlade', code='97501', zipcode='97500',
149
                                    population=42)
150

  
151

  
152
@pytest.fixture
153
def mock_update_api_geo():
154
    with mock.patch('passerelle.apps.base_adresse.models.BaseAdresse.update_api_geo_data',
155
                    new=lambda x: None) as _fixture:
156
        yield _fixture
157

  
158

  
159
@pytest.fixture
160
def mock_update_streets():
161
    with mock.patch('passerelle.apps.base_adresse.models.BaseAdresse.update_streets_data',
162
                    new=lambda x: None) as _fixture:
163
        yield _fixture
164

  
165

  
77 166
@mock.patch('passerelle.utils.Request.get')
78 167
def test_base_adresse_search(mocked_get, app, base_adresse):
79 168
    endpoint = utils.generic_endpoint_url('base-adresse', 'search', slug=base_adresse.slug)
......
195 284
    assert len(resp.json['data']) == 0
196 285

  
197 286

  
287
@pytest.mark.usefixtures('mock_update_api_geo')
198 288
@mock.patch('passerelle.utils.Request.get')
199 289
def test_base_adresse_command_update(mocked_get, db, base_adresse):
200 290
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
......
214 304
    assert mocked_get.call_count == 2
215 305

  
216 306

  
307
@pytest.mark.usefixtures('mock_update_api_geo')
217 308
@mock.patch('passerelle.utils.Request.get')
218 309
def test_base_adresse_command_hourly_update(mocked_get, db, base_adresse):
310
    base_adresse.update_api_geo_data = lambda: None
219 311
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
220 312
    mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
221 313
    # check the first hourly job downloads streets
......
227 319
    assert mocked_get.call_count == 1
228 320

  
229 321

  
322
@pytest.mark.usefixtures('mock_update_api_geo')
230 323
@mock.patch('passerelle.utils.Request.get')
231 324
def test_base_adresse_command_update_97x(mocked_get, db, base_adresse_97x):
325
    base_adresse_97x.update_api_geo_data = lambda: None
232 326
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
233 327
    mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
234 328
    call_command('cron', 'daily')
......
236 330
    assert StreetModel.objects.count() == 2
237 331

  
238 332

  
333
@pytest.mark.usefixtures('mock_update_api_geo')
239 334
@mock.patch('passerelle.utils.Request.get')
240 335
def test_base_adresse_command_update_corsica(mocked_get, db, base_adresse_corsica):
336
    base_adresse_corsica.update_api_geo_data = lambda: None
241 337
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
242 338
    mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
243 339
    call_command('cron', 'daily')
......
247 343
    assert StreetModel.objects.count() == 0
248 344

  
249 345

  
346
@pytest.mark.usefixtures('mock_update_api_geo')
250 347
@mock.patch('passerelle.utils.Request.get')
251 348
def test_base_adresse_command_update_multiple(mocked_get, db, base_adresse_multiple):
349
    base_adresse_multiple.update_api_geo_data = lambda: None
252 350
    filepath = os.path.join(os.path.dirname(__file__), 'data', 'update_streets_test.bz2')
253 351
    mocked_get.return_value = utils.FakedResponse(content=open(filepath).read(), status_code=200)
254 352
    call_command('cron', 'daily')
......
258 356
    mocked_get.assert_any_call('http://bano.openstreetmap.fr/BAN_odbl/BAN_odbl_2A-json.bz2')
259 357
    mocked_get.assert_any_call('http://bano.openstreetmap.fr/BAN_odbl/BAN_odbl_2B-json.bz2')
260 358
    assert StreetModel.objects.count() == 5
359

  
360

  
361
def test_base_adresse_cities(app, base_adresse, city, department, region):
362
    resp = app.get('/base-adresse/%s/cities?q=chambe' % base_adresse.slug)
363
    result = resp.json['data'][0]
364
    assert result['name'] == city.name
365
    assert result['text'] == '%s %s' % (city.zipcode, city.name)
366
    assert result['code'] == city.code
367
    assert result['zipcode'] == city.zipcode
368
    assert result['id'] == '%s.%s' % (city.code, city.zipcode)
369
    assert result['population'] == city.population
370
    assert result['region_code'] == city.region.code
371
    assert result['region_name'] == city.region.name
372
    assert result['department_code'] == city.department.code
373
    assert result['department_name'] == city.department.name
374

  
375
    resp = app.get('/base-adresse/%s/cities?q=73' % base_adresse.slug)
376
    assert resp.json['data'][0] == result
377

  
378
    resp = app.get('/base-adresse/%s/cities?code=73065' % base_adresse.slug)
379
    assert resp.json['data'][0] == result
380

  
381

  
382
def test_base_adresse_cities_missing_region_and_department(app, base_adresse, miquelon):
383
    resp = app.get('/base-adresse/%s/cities?q=miqu' % base_adresse.slug)
384
    result = resp.json['data'][0]
385
    assert result['name'] == miquelon.name
386
    assert not result['department_code']
387
    assert not result['region_code']
388
    assert not result['department_name']
389
    assert not result['region_name']
390

  
391

  
392
def test_base_adresse_cities_region_department(app, base_adresse, miquelon, city):
393
    reg = RegionModel.objects.create(name=u'IdF', code='11')
394
    dep = DepartmentModel.objects.create(name=u'Paris', code='75', region=reg)
395
    paris = CityModel.objects.create(name=u'Paris', code='75056', zipcode='75014',
396
                                        population=2000000, region=reg, department=dep)
397

  
398
    resp = app.get('/base-adresse/%s/cities?department_code=73' % base_adresse.slug)
399
    result = resp.json['data']
400
    assert len(result) == 1
401
    assert result[0]['name'] == city.name
402

  
403
    resp = app.get('/base-adresse/%s/cities?region_code=84' % base_adresse.slug)
404
    result = resp.json['data']
405
    assert len(result) == 1
406
    assert result[0]['name'] == city.name
407

  
408
    resp = app.get('/base-adresse/%s/cities?region_code=84&department_code=75' % base_adresse.slug)
409
    result = resp.json['data']
410
    assert not result
411

  
412

  
413
def test_base_adresse_cities_sort_order(app, base_adresse, miquelon, city):
414
    assert miquelon.population < city.population
415
    resp = app.get('/base-adresse/%s/cities' % base_adresse.slug)
416
    result = resp.json['data']
417
    assert result[0]['name'] == city.name
418
    assert result[1]['name'] == miquelon.name
419

  
420

  
421
def test_base_adresse_cities_get_by_id(app, base_adresse, city):
422
    for i in range(1, 10):
423
        # create additional cities
424
        city.pk = None
425
        city.zipcode = int(city.zipcode) + i
426
        city.save()
427

  
428
    resp = app.get('/base-adresse/%s/cities?q=cham' % base_adresse.slug)
429
    result = resp.json['data'][0]
430
    assert len(resp.json['data']) == 10
431
    city_id = result['id']
432

  
433
    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, city_id))
434
    assert len(resp.json['data']) == 1
435
    result2 = resp.json['data'][0]
436
    assert result2['text'] == result['text']
437

  
438
    # non integer id.
439
    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, 'XXX'))
440
    assert resp.json['err'] == 1
441

  
442
    # integer but without match.
443
    resp = app.get('/base-adresse/%s/cities?id=%s' % (base_adresse.slug, '1.1'))
444
    assert len(resp.json['data']) == 0
445

  
446

  
447
def test_base_adresse_departments(app, base_adresse, department, region):
448
    resp = app.get('/base-adresse/%s/departments?q=sav' % base_adresse.slug)
449
    result = resp.json['data'][0]
450
    assert result['name'] == department.name
451
    assert result['code'] == department.code
452
    assert result['id'] == department.code
453
    assert result['text'] == '%s %s' % (department.code, department.name)
454
    assert result['region_code'] == region.code
455
    assert result['region_name'] == region.name
456

  
457
    resp = app.get('/base-adresse/%s/departments?q=73' % base_adresse.slug)
458
    result = resp.json['data'][0]
459
    assert result['name'] == department.name
460

  
461
    resp = app.get('/base-adresse/%s/departments?id=%s' % (base_adresse.slug, department.code))
462
    result = resp.json['data'][0]
463
    assert result['name'] == department.name
464

  
465

  
466
def test_base_adresse_departments_region(app, base_adresse, department):
467
    reg = RegionModel.objects.create(name=u'IdF', code='11')
468
    paris = DepartmentModel.objects.create(name=u'Paris', code='75', region=reg)
469

  
470
    resp = app.get('/base-adresse/%s/departments?region_code=84' % base_adresse.slug)
471
    result = resp.json['data']
472
    assert len(result) == 1
473
    assert result[0]['name'] == department.name
474

  
475

  
476
def test_base_adresse_regions(app, base_adresse, region):
477
    resp = app.get('/base-adresse/%s/regions?q=au' % base_adresse.slug)
478
    result = resp.json['data'][0]
479
    assert result['name'] == region.name
480
    assert result['code'] == region.code
481
    assert result['id'] == region.code
482
    assert result['text'] == '%s %s' % (region.code, region.name)
483

  
484
    resp = app.get('/base-adresse/%s/regions?id=%s' % (base_adresse.slug, region.code))
485
    result = resp.json['data'][0]
486
    assert result['name'] == region.name
487

  
488

  
489
@pytest.mark.usefixtures('mock_update_streets')
490
@mock.patch('passerelle.utils.Request.get')
491
def test_base_adresse_command_update_geo(mocked_get, db, base_adresse):
492
    return_values = [utils.FakedResponse(content=content, status_code=200)
493
                     for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, FAKE_API_GEO)]
494
    mocked_get.side_effect = return_values
495
    call_command('cron', 'daily')
496
    assert mocked_get.call_count == 3
497
    mocked_get.assert_any_call(urljoin(base_adresse.api_geo_url, 'communes'))
498
    mocked_get.assert_any_call(urljoin(base_adresse.api_geo_url, 'regions'))
499
    mocked_get.assert_any_call(urljoin(base_adresse.api_geo_url, 'departements'))
500

  
501
    regions = RegionModel.objects.all()
502
    assert regions.count() == 2
503
    idf = regions.get(name='Île-de-France')
504
    assert idf.code == '11'
505
    centre = regions.get(name='Bourgogne-Franche-Comté')
506
    assert centre.code == '27'
507

  
508
    departments = DepartmentModel.objects.all()
509
    assert departments.count() == 2
510
    paris_dep = departments.get(name='Paris')
511
    assert paris_dep.code == '75'
512
    assert paris_dep.region == idf
513
    nievre = departments.get(name='Nièvre')
514
    assert nievre.code == '58'
515
    assert nievre.region == centre
516

  
517
    cities = CityModel.objects.all()
518
    assert cities.count() == 3
519
    paris = cities.get(zipcode='75001')
520
    assert paris.name == 'Paris'
521
    assert paris.code == '75056'
522
    assert paris.population == 2190327
523
    assert paris.department.code == '75'
524
    assert paris.region.code == '11'
525

  
526
    paris2 = cities.get(zipcode='75002')
527
    paris_json = paris.to_json()
528
    for key, value in paris2.to_json().items():
529
        if not key in ['id', 'text', 'zipcode']:
530
            assert paris_json[key] == value
531

  
532
    miquelon = cities.get(zipcode='97500')
533
    assert miquelon.name == 'Miquelon-Langlade'
534
    assert miquelon.code == '97501'
535
    assert miquelon.population == 596
536
    assert not miquelon.department
537
    assert not miquelon.region
538

  
539
    # check a new call downloads again
540
    mocked_get.side_effect = return_values
541
    call_command('cron', 'daily')
542
    assert mocked_get.call_count == 6
543
    # and doesn't delete anything
544
    assert CityModel.objects.count() == 3
545
    assert DepartmentModel.objects.count() == 2
546
    assert RegionModel.objects.count() == 2
547

  
548

  
549
@pytest.mark.usefixtures('mock_update_streets')
550
@mock.patch('passerelle.utils.Request.get')
551
def test_base_adresse_command_update_geo_delete(mocked_get, db, base_adresse):
552
    return_values = [utils.FakedResponse(content=content, status_code=200)
553
                     for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, FAKE_API_GEO)]
554
    mocked_get.side_effect = return_values
555
    call_command('cron', 'daily')
556
    assert CityModel.objects.count() == 3
557

  
558
    new_fake_api_geo = json.dumps([FAKE_API_GEO_LIST[1]])
559
    return_values = [utils.FakedResponse(content=content, status_code=200)
560
                     for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, new_fake_api_geo)]
561
    mocked_get.side_effect = return_values
562
    call_command('cron', 'daily')
563
    assert CityModel.objects.count() == 1
564

  
565

  
566
@pytest.mark.usefixtures('mock_update_streets')
567
@mock.patch('passerelle.utils.Request.get')
568
def test_base_adresse_command_hourly_update_geo(mocked_get, db, base_adresse):
569
    return_values = [utils.FakedResponse(content=content, status_code=200)
570
                     for content in (FAKE_API_GEO_REGIONS, FAKE_API_GEO_DEPARTMENTS, FAKE_API_GEO)]
571
    mocked_get.side_effect = return_values
572
    # check the first hourly job downloads data
573
    call_command('cron', 'hourly')
574
    assert mocked_get.call_count == 3
575
    assert CityModel.objects.count() == 3
576
    # check a second call doesn't download anything
577
    call_command('cron', 'hourly')
578
    assert mocked_get.call_count == 3
579

  
580

  
581
@pytest.mark.usefixtures('mock_update_streets')
582
@mock.patch('passerelle.utils.Request.get')
583
def test_base_adresse_command_update_geo_invalid(mocked_get, db, base_adresse):
584
    mocked_get.return_value = utils.FakedResponse(content='{}', status_code=200)
585
    with pytest.raises(CommandError):
586
        call_command('cron', 'daily')
587
    assert mocked_get.call_count == 1
588
    assert not RegionModel.objects.exists()
589

  
590
    mocked_get.return_value = utils.FakedResponse(content=FAKE_API_GEO, status_code=500)
591
    call_command('cron', 'daily')
592
    assert mocked_get.call_count == 4
593
    assert not RegionModel.objects.exists()
594

  
595
    mocked_get.return_value = utils.FakedResponse(content='not-json', status_code=200)
596
    call_command('cron', 'daily')
597
    assert mocked_get.call_count == 7
598
    assert not RegionModel.objects.exists()
599

  
600

  
601
@pytest.mark.usefixtures('mock_update_streets')
602
@mock.patch('passerelle.utils.Request.get', side_effect=ConnectionError)
603
def test_base_adresse_command_update_geo_no_connection(mocked_get, db, base_adresse):
604
    call_command('cron', 'daily')
605
    assert mocked_get.call_count == 3
606
    assert not RegionModel.objects.exists()
261
-