photon: new geographic information connector (#50935)
This commit is contained in:
parent
2081c3d37e
commit
2d649b2247
|
@ -0,0 +1,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.18 on 2021-02-07 22:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.contrib.postgres.fields.jsonb
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('base', '0029_auto_20210202_1627'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AddressCacheModel',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('api_id', models.CharField(max_length=32, unique=True)),
|
||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||
('timestamp', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Photon',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=50, verbose_name='Title')),
|
||||
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
('service_url', models.CharField(default='https://photon.komoot.io/', help_text='Base Adresse Web Service URL', max_length=128, verbose_name='Service URL')),
|
||||
('latitude', models.FloatField(blank=True, help_text='Geographic priority for /addresses/ endpoint.', null=True, verbose_name='Latitude')),
|
||||
('longitude', models.FloatField(blank=True, help_text='Geographic priority for /addresses/ endpoint.', null=True, verbose_name='Longitude')),
|
||||
('users', models.ManyToManyField(blank=True, related_name='_photon_users_+', related_query_name='+', to='base.ApiUser')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Photon Web Service',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,228 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2021 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
from requests import RequestException
|
||||
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.db import models
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from django.utils.six.moves.urllib import parse as urlparse
|
||||
|
||||
from passerelle.base.models import BaseResource
|
||||
from passerelle.utils.api import endpoint
|
||||
from passerelle.utils.jsonresponse import APIError
|
||||
|
||||
|
||||
class AddressCacheModel(models.Model):
|
||||
api_id = models.CharField(max_length=32, unique=True)
|
||||
data = JSONField()
|
||||
timestamp = models.DateTimeField(auto_now=True)
|
||||
|
||||
def update_timestamp(self):
|
||||
self.save()
|
||||
|
||||
|
||||
class Photon(BaseResource):
|
||||
service_url = models.CharField(
|
||||
max_length=128,
|
||||
blank=False,
|
||||
default='https://photon.komoot.io/',
|
||||
verbose_name=_('Service URL'),
|
||||
help_text=_('Base Adresse Web Service URL'),
|
||||
)
|
||||
latitude = models.FloatField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('Latitude'),
|
||||
help_text=_('Geographic priority for /addresses/ endpoint.'),
|
||||
)
|
||||
longitude = models.FloatField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('Longitude'),
|
||||
help_text=_('Geographic priority for /addresses/ endpoint.'),
|
||||
)
|
||||
|
||||
category = _('Geographic information system')
|
||||
|
||||
api_description = _(
|
||||
'photon is an open source geocoder built for OpenStreetMap data. '
|
||||
'It provides search-as-you-type and multilingual support.'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Photon')
|
||||
|
||||
@staticmethod
|
||||
def format_address_data(data):
|
||||
result = {}
|
||||
result['lon'] = str(data['geometry']['coordinates'][0])
|
||||
result['lat'] = str(data['geometry']['coordinates'][1])
|
||||
result['address'] = {}
|
||||
for prop, value in data['properties'].items():
|
||||
if prop in ('country', 'city', 'postcode'):
|
||||
result['address'][prop] = value
|
||||
elif prop == 'housenumber':
|
||||
result['address']['house_number'] = value
|
||||
elif prop == 'street':
|
||||
result['address']['road'] = value
|
||||
|
||||
result['text'] = ''
|
||||
if result['address'].get('house_number'):
|
||||
result['text'] += '%s, ' % result['address']['house_number']
|
||||
if result['address'].get('road'):
|
||||
result['text'] += '%s ' % result['address']['road']
|
||||
if result['address'].get('postcode'):
|
||||
result['text'] += '%s ' % result['address']['postcode']
|
||||
if result['address'].get('city'):
|
||||
result['text'] += '%s' % result['address']['city']
|
||||
result['display_name'] = result['text']
|
||||
result['id'] = data['properties']['osm_id']
|
||||
return result
|
||||
|
||||
@endpoint(
|
||||
pattern='(?P<q>.+)?$',
|
||||
description=_('Addresses list'),
|
||||
parameters={
|
||||
'id': {'description': _('Address identifier')},
|
||||
'q': {'description': _('Address'), 'example_value': '169 rue du chateau, paris'},
|
||||
'page_limit': {
|
||||
'description': _('Maximum number of results to return. Must be ' 'lower than 20.')
|
||||
},
|
||||
'zipcode': {'description': _('Zipcode'), 'example_value': '75014'},
|
||||
'lat': {'description': _('Prioritize results according to coordinates.')},
|
||||
'lon': {'description': _('Prioritize results according to coordinates.')},
|
||||
},
|
||||
)
|
||||
def addresses(
|
||||
self, request, id=None, q=None, zipcode=None, lat=None, lon=None, page_limit=5, lang='fr', bbox=None
|
||||
):
|
||||
if id is not None:
|
||||
try:
|
||||
address = AddressCacheModel.objects.get(api_id=id)
|
||||
except AddressCacheModel.DoesNotExist:
|
||||
return {'err': _('Address ID not found')}
|
||||
address.update_timestamp()
|
||||
return {'data': [address.data]}
|
||||
|
||||
if not q:
|
||||
return {'data': []}
|
||||
|
||||
try:
|
||||
if int(page_limit) > 20:
|
||||
page_limit = 20
|
||||
except ValueError:
|
||||
page_limit = 5
|
||||
|
||||
scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.service_url)
|
||||
path = urlparse.urljoin(path, 'api/')
|
||||
query_args = {'q': q, 'limit': page_limit, 'lang': lang}
|
||||
if self.latitude and self.longitude or lat and lon:
|
||||
query_args['lat'] = lat or self.latitude
|
||||
query_args['lon'] = lon or self.longitude
|
||||
if bbox:
|
||||
query_args['bbox'] = bbox
|
||||
query = urlencode(query_args)
|
||||
url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
|
||||
|
||||
result_response = self.requests.get(url)
|
||||
try:
|
||||
result_response.raise_for_status()
|
||||
except RequestException as e:
|
||||
raise APIError('Bad response code from API: %s' % e)
|
||||
|
||||
result = []
|
||||
|
||||
for feature in result_response.json().get('features'):
|
||||
if not feature['geometry']['type'] == 'Point':
|
||||
continue # skip unknown
|
||||
if zipcode and feature['properties'].get('postcode') != zipcode:
|
||||
continue
|
||||
data = self.format_address_data(feature)
|
||||
result.append(data)
|
||||
address, created = AddressCacheModel.objects.get_or_create(
|
||||
api_id=data['id'], defaults={'data': data}
|
||||
)
|
||||
if not created:
|
||||
address.update_timestamp()
|
||||
|
||||
return {'data': result}
|
||||
|
||||
@endpoint(
|
||||
description=_('Geocoding (Nominatim API)'),
|
||||
pattern='(?P<q>.+)?$',
|
||||
parameters={
|
||||
'q': {'description': _('Address'), 'example_value': '169 rue du chateau, paris'},
|
||||
'zipcode': {'description': _('Zipcode')},
|
||||
'lat': {'description': _('Prioritize results according to coordinates.')},
|
||||
'lon': {'description': _('Prioritize results according to coordinates.')},
|
||||
},
|
||||
)
|
||||
def search(self, request, q, zipcode=None, lat=None, lon=None, **kwargs):
|
||||
if kwargs.get('format', 'json') != 'json':
|
||||
raise NotImplementedError()
|
||||
lang = kwargs.get('accept-language', 'fr')
|
||||
bbox = kwargs.get('viewbox')
|
||||
result = self.addresses(
|
||||
request, q=q, zipcode=zipcode, lat=lat, lon=lon, page_limit=1, lang=lang, bbox=bbox
|
||||
)
|
||||
return result['data']
|
||||
|
||||
@endpoint(
|
||||
description=_('Reverse geocoding (Nominatim API)'),
|
||||
parameters={
|
||||
'lat': {'description': _('Latitude'), 'example_value': 48.833708},
|
||||
'lon': {'description': _('Longitude'), 'example_value': 2.323349},
|
||||
},
|
||||
)
|
||||
def reverse(self, request, lat, lon, **kwargs):
|
||||
if kwargs.get('format', 'json') != 'json':
|
||||
raise NotImplementedError()
|
||||
|
||||
scheme, netloc, path, params, query, fragment = urlparse.urlparse(self.service_url)
|
||||
path = urlparse.urljoin(path, 'reverse/')
|
||||
query = urlencode({'lat': lat, 'lon': lon})
|
||||
url = urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
|
||||
|
||||
result_response = self.requests.get(url)
|
||||
try:
|
||||
result_response.raise_for_status()
|
||||
except RequestException as e:
|
||||
raise APIError('Bad response code from API: %s' % e)
|
||||
result = None
|
||||
|
||||
for feature in result_response.json().get('features'):
|
||||
if not feature['geometry']['type'] == 'Point':
|
||||
continue # skip unknown
|
||||
result = self.format_address_data(feature)
|
||||
break
|
||||
return result
|
||||
|
||||
def check_status(self):
|
||||
self.search(None, 'passerelle status check')
|
||||
|
||||
def clean_addresses_cache(self):
|
||||
old_addresses = AddressCacheModel.objects.filter(
|
||||
timestamp__lt=timezone.now() - datetime.timedelta(hours=1)
|
||||
)
|
||||
old_addresses.delete()
|
||||
|
||||
def hourly(self):
|
||||
super().hourly()
|
||||
self.clean_addresses_cache()
|
|
@ -155,6 +155,7 @@ INSTALLED_APPS = (
|
|||
'passerelle.apps.ovh',
|
||||
'passerelle.apps.oxyd',
|
||||
'passerelle.apps.phonecalls',
|
||||
'passerelle.apps.photon',
|
||||
'passerelle.apps.solis',
|
||||
'passerelle.apps.twilio',
|
||||
'passerelle.apps.vivaticket',
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2021 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
from httmock import urlmatch, HTTMock, response, remember_called
|
||||
import json
|
||||
import mock
|
||||
import pytest
|
||||
import utils
|
||||
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
from passerelle.apps.photon.models import Photon, AddressCacheModel
|
||||
|
||||
|
||||
CONTENT = {
|
||||
"features": [
|
||||
{
|
||||
"geometry": {"coordinates": [4.8522272, 45.7587414], "type": "Point"},
|
||||
"properties": {
|
||||
"city": "Lyon 3ème Arrondissement",
|
||||
"country": "France",
|
||||
"housenumber": "208",
|
||||
"osm_id": 154419,
|
||||
"osm_key": "place",
|
||||
"osm_type": "N",
|
||||
"osm_value": "house",
|
||||
"postcode": "69003",
|
||||
"street": "Rue Garibaldi",
|
||||
"type": "house",
|
||||
},
|
||||
"type": "Feature",
|
||||
},
|
||||
{
|
||||
"geometry": {"coordinates": [4.8522681, 45.7585214], "type": "Point"},
|
||||
"properties": {
|
||||
"city": "Lyon 3ème Arrondissement",
|
||||
"country": "France",
|
||||
"housenumber": "208bis",
|
||||
"osm_id": 153400,
|
||||
"osm_key": "place",
|
||||
"osm_type": "N",
|
||||
"osm_value": "house",
|
||||
"postcode": "69003",
|
||||
"street": "Rue Garibaldi",
|
||||
"type": "house",
|
||||
},
|
||||
"type": "Feature",
|
||||
},
|
||||
],
|
||||
"type": "FeatureCollection",
|
||||
}
|
||||
|
||||
FAKED_CONTENT = json.dumps(CONTENT)
|
||||
|
||||
|
||||
@urlmatch(netloc='^photon.komoot.io$', path='^/api/$')
|
||||
@remember_called
|
||||
def photon_search(url, request):
|
||||
return response(200, CONTENT, request=request)
|
||||
|
||||
|
||||
@urlmatch(netloc='^photon.komoot.io$', path='^/reverse/$')
|
||||
def photon_reverse(url, request):
|
||||
return response(200, CONTENT, request=request)
|
||||
|
||||
|
||||
@pytest.yield_fixture
|
||||
def mock_photon_search():
|
||||
with HTTMock(photon_search):
|
||||
yield photon_search
|
||||
|
||||
|
||||
@pytest.yield_fixture
|
||||
def mock_photon_reverse():
|
||||
with HTTMock(photon_reverse):
|
||||
yield None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def photon(db):
|
||||
return utils.setup_access_rights(Photon.objects.create(slug='test'))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def photon_coordinates(db):
|
||||
return utils.setup_access_rights(Photon.objects.create(slug='test', latitude=1.2, longitude=2.1))
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_search(mocked_get, app, photon):
|
||||
photon.service_url = 'http://example.net/path/'
|
||||
photon.save()
|
||||
endpoint = utils.generic_endpoint_url('photon', 'search', slug=photon.slug)
|
||||
mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
|
||||
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
||||
assert mocked_get.call_args[0][0].startswith('http://example.net/path/api/?')
|
||||
data = resp.json[0]
|
||||
assert data['lat'] == '45.7587414'
|
||||
assert data['lon'] == '4.8522272'
|
||||
assert data['display_name'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
||||
|
||||
|
||||
def test_photon_search_qs(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/search?q=plop' % photon.slug)
|
||||
assert 'display_name' in resp.json[0]
|
||||
|
||||
|
||||
def test_photon_search_qs_zipcode(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/search?q=plop&zipcode=69003' % photon.slug)
|
||||
assert 'display_name' in resp.json[0]
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_search_qs_lat_lon(mocked_get, app, photon):
|
||||
app.get('/photon/%s/search?q=plop&lat=0&lon=1' % photon.slug)
|
||||
assert 'lat=0' in mocked_get.call_args[0][0]
|
||||
assert 'lon=1' in mocked_get.call_args[0][0]
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_search_qs_viewbox(mocked_get, app, photon):
|
||||
app.get('/photon/%s/search?q=plop&viewbox=4.830,45.753,4.831,45.754&bounded=1' % photon.slug)
|
||||
assert (
|
||||
mocked_get.call_args[0][0]
|
||||
== 'https://photon.komoot.io/api/?q=plop&limit=1&lang=fr&bbox=4.830%2C45.753%2C4.831%2C45.754'
|
||||
)
|
||||
|
||||
|
||||
def test_photon_search_qs_empty(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/search?q=' % photon.slug)
|
||||
assert len(resp.json) == 0
|
||||
|
||||
|
||||
def test_photon_search_qs_parameters_error(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/search' % photon.slug, status=400)
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_class'] == 'passerelle.views.WrongParameter'
|
||||
assert resp.json['err_desc'] == u"missing parameters: 'q'."
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_api_error(mocked_get, app, photon):
|
||||
def raise_for_status():
|
||||
raise HTTPError("400 Client Error: Bad Request for url: xxx")
|
||||
|
||||
response = utils.FakedResponse(content=json.dumps({'title': 'error'}), status_code=400)
|
||||
response.raise_for_status = raise_for_status
|
||||
mocked_get.return_value = response
|
||||
resp = app.get('/photon/%s/search' % photon.slug, params={'q': 'plop'}, status=200)
|
||||
|
||||
assert resp.json['err'] == 1
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_reverse_path(mocked_get, app, photon):
|
||||
mocked_get.return_value = utils.FakedResponse(content=json.dumps({'features': []}), status_code=200)
|
||||
photon.service_url = 'http://example.net/path/'
|
||||
photon.save()
|
||||
app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
|
||||
assert mocked_get.call_args[0][0].startswith('http://example.net/path/reverse/?')
|
||||
|
||||
|
||||
def test_photon_reverse(app, photon, mock_photon_reverse):
|
||||
resp = app.get('/photon/%s/reverse?lon=4.8522272&lat=45.7587414' % photon.slug)
|
||||
data = resp.json
|
||||
assert 'display_name' in data
|
||||
assert data['address']['city'] == 'Lyon 3ème Arrondissement'
|
||||
assert data['address']['postcode'] == '69003'
|
||||
assert data['address']['house_number'] == '208'
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_addresses(mocked_get, app, photon):
|
||||
endpoint = utils.generic_endpoint_url('photon', 'addresses', slug=photon.slug)
|
||||
assert endpoint == '/photon/test/addresses'
|
||||
mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
|
||||
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
||||
data = resp.json['data'][0]
|
||||
assert data['lat'] == '45.7587414'
|
||||
assert data['lon'] == '4.8522272'
|
||||
assert data['display_name'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
||||
assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
||||
assert data['id'] == 154419
|
||||
assert data['address']['city'] == 'Lyon 3ème Arrondissement'
|
||||
assert data['address']['postcode'] == '69003'
|
||||
assert data['address']['road'] == 'Rue Garibaldi'
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_addresses_qs_page_limit(mocked_get, app, photon):
|
||||
resp = app.get('/photon/%s/addresses?q=plop&page_limit=1' % photon.slug)
|
||||
assert 'limit=1' in mocked_get.call_args[0][0]
|
||||
|
||||
resp = app.get('/photon/%s/addresses?q=plop&page_limit=100' % photon.slug)
|
||||
assert 'limit=20' in mocked_get.call_args[0][0]
|
||||
|
||||
resp = app.get('/photon/%s/addresses?q=plop&page_limit=blabla' % photon.slug, status=400)
|
||||
assert 'invalid value' in resp.json['err_desc']
|
||||
|
||||
|
||||
def test_photon_addresses_qs_zipcode(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/addresses?q=plop&zipcode=69003' % photon.slug)
|
||||
assert 'display_name' in resp.json['data'][0]
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_addresses_qs_coordinates(mocked_get, app, photon_coordinates):
|
||||
resp = app.get('/photon/%s/addresses?q=plop' % photon_coordinates.slug)
|
||||
assert 'lat=%s' % photon_coordinates.latitude in mocked_get.call_args[0][0]
|
||||
assert 'lon=%s' % photon_coordinates.longitude in mocked_get.call_args[0][0]
|
||||
|
||||
resp = app.get('/photon/%s/addresses?q=plop&lat=42&lon=43' % photon_coordinates.slug)
|
||||
assert 'lat=42' in mocked_get.call_args[0][0]
|
||||
assert 'lon=43' in mocked_get.call_args[0][0]
|
||||
|
||||
|
||||
def test_photon_addresses_cache(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
||||
assert mock_photon_search.call['count'] == 1
|
||||
|
||||
data = resp.json['data'][0]
|
||||
assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
||||
|
||||
api_id = data['id']
|
||||
assert AddressCacheModel.objects.filter(api_id=api_id).exists()
|
||||
assert AddressCacheModel.objects.count() == 2
|
||||
|
||||
resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, api_id))
|
||||
assert mock_photon_search.call['count'] == 1 # no new call
|
||||
assert data['text'] == '208, Rue Garibaldi 69003 Lyon 3ème Arrondissement'
|
||||
assert 'address' in data
|
||||
|
||||
resp = app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
||||
assert AddressCacheModel.objects.count() == 2 # no new object has been created
|
||||
|
||||
|
||||
def test_photon_addresses_cache_err(app, photon, mock_photon_search):
|
||||
resp = app.get('/photon/%s/addresses?id=%s' % (photon.slug, 'wrong_id'))
|
||||
assert mock_photon_search.call['count'] == 0
|
||||
assert 'err' in resp.json
|
||||
|
||||
|
||||
def test_photon_addresses_clean_cache(app, photon, freezer, mock_photon_search):
|
||||
app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
||||
assert AddressCacheModel.objects.count() == 2
|
||||
|
||||
freezer.move_to(datetime.timedelta(minutes=30))
|
||||
call_command('cron', 'hourly')
|
||||
assert AddressCacheModel.objects.count() == 2
|
||||
|
||||
freezer.move_to(datetime.timedelta(minutes=30, seconds=1))
|
||||
call_command('cron', 'hourly')
|
||||
assert AddressCacheModel.objects.count() == 0
|
||||
|
||||
app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
||||
assert AddressCacheModel.objects.count() == 2
|
||||
|
||||
# asking for the address again resets the timestamp
|
||||
freezer.move_to(datetime.timedelta(hours=1, seconds=1))
|
||||
app.get('/photon/%s/addresses?q=plop' % photon.slug)
|
||||
call_command('cron', 'hourly')
|
||||
assert AddressCacheModel.objects.count() == 2
|
||||
|
||||
freezer.move_to(datetime.timedelta(hours=1, seconds=1))
|
||||
app.get('/photon/%s/addresses?id=%s' % (photon.slug, '154419'))
|
||||
call_command('cron', 'hourly')
|
||||
assert AddressCacheModel.objects.count() == 1
|
||||
|
||||
|
||||
@mock.patch('passerelle.utils.Request.get')
|
||||
def test_photon_addresses_data_change(mocked_get, app, photon):
|
||||
endpoint = utils.generic_endpoint_url('photon', 'addresses', slug=photon.slug)
|
||||
mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT, status_code=200)
|
||||
|
||||
# one user selects an address
|
||||
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
||||
data = resp.json['data'][0]
|
||||
address_id, address_text = data['id'], data['text']
|
||||
|
||||
# another requests the same while upstream data has been updated
|
||||
new_content = json.loads(FAKED_CONTENT)
|
||||
new_content['features'][0]['properties']['housenumber'] = 'changed'
|
||||
mocked_get.return_value = utils.FakedResponse(content=json.dumps(new_content), status_code=200)
|
||||
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
||||
|
||||
# first user saves the form, data should not have changed
|
||||
resp = app.get(endpoint, params={'id': address_id}, status=200)
|
||||
assert resp.json['data'][0]['text'] == address_text
|
||||
|
||||
# when cache is cleared, we get the updated data
|
||||
AddressCacheModel.objects.all().delete()
|
||||
resp = app.get(endpoint, params={'q': 'plop'}, status=200)
|
||||
assert 'changed' in resp.json['data'][0]['text']
|
Loading…
Reference in New Issue