294 lines
13 KiB
Python
294 lines
13 KiB
Python
# passerelle - uniform access to multiple data sources and services
|
|
# Copyright (C) 2017 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 json
|
|
|
|
from django.db import models
|
|
from django.http import HttpResponse
|
|
from django.utils.http import urlencode
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from passerelle.base.models import BaseResource
|
|
from passerelle.utils.api import endpoint
|
|
from passerelle.utils.jsonresponse import APIError
|
|
|
|
|
|
class Okina(BaseResource):
|
|
service_url = models.URLField(
|
|
max_length=256, blank=False, verbose_name=_('Service URL'), help_text=_('Okina API base URL')
|
|
)
|
|
username = models.CharField(max_length=128, blank=False, verbose_name=_('Username'))
|
|
password = models.CharField(max_length=128, blank=False, verbose_name=_('Password'))
|
|
|
|
category = _('Business Process Connectors')
|
|
|
|
class Meta:
|
|
verbose_name = _('Okina')
|
|
|
|
def request(self, endpoint, payload=None, result_is_json=True, result_is_list=True):
|
|
url = self.service_url + endpoint
|
|
auth = (self.username, self.password)
|
|
headers = {}
|
|
if result_is_json:
|
|
headers['Accept'] = 'application/json'
|
|
if payload is None:
|
|
response = self.requests.get(url, auth=auth, headers=headers)
|
|
else:
|
|
headers['Content-Type'] = 'application/json'
|
|
data = json.dumps(payload)
|
|
response = self.requests.post(url, data=data, auth=auth, headers=headers)
|
|
if not result_is_json:
|
|
if response.status_code // 100 != 2:
|
|
try:
|
|
data = response.json()
|
|
except ValueError:
|
|
data = {'message': 'HTTP request failed'}
|
|
raise APIError('%s (HTTP error %s)' % (data.get('message'), response.status_code), data=data)
|
|
return response
|
|
try:
|
|
data = response.json()
|
|
except ValueError:
|
|
raise APIError('bad JSON response: %r' % response.content)
|
|
if response.status_code in (401, 403):
|
|
raise APIError('%s (%s)' % (data.get('message'), response.status_code), data=data)
|
|
if result_is_list and not isinstance(data, list):
|
|
raise APIError('response is not a list', data=data)
|
|
if response.status_code // 100 != 2:
|
|
data = {
|
|
'status_code': response.status_code,
|
|
'payload': data,
|
|
}
|
|
raise APIError('HTTP request failed, error %s' % response.status_code, data=data)
|
|
return data
|
|
|
|
@endpoint(perm='OPEN')
|
|
def cities(self, request):
|
|
okina_cities = self.request('cities')
|
|
cities = []
|
|
for city in okina_cities:
|
|
if 'name' in city:
|
|
city['text'] = '%(name)s (%(zipCode)s)' % city
|
|
else: # old API
|
|
city = city['cityObject']
|
|
city['text'] = '%(nameCity)s (%(zipCode)s)' % city
|
|
city['id'] = '%s' % city['id']
|
|
city['lat'] = city.get('latitude')
|
|
city['lon'] = city.get('longitude')
|
|
cities.append(city)
|
|
cities.sort(key=lambda x: x['text'])
|
|
return {'data': cities}
|
|
|
|
@endpoint(perm='OPEN')
|
|
def classes(self, request):
|
|
return {
|
|
'data': [{'id': '%s' % item['id'], 'text': item['label']} for item in self.request('classes')]
|
|
}
|
|
|
|
def get_institutions(self, query=''):
|
|
okina_institutions = self.request('institutions' + query)
|
|
institutions = []
|
|
for institution in okina_institutions:
|
|
institution.update(institution['schoolEstablishment'])
|
|
del institution['schoolEstablishment']
|
|
institution['id'] = '%s' % institution['id']
|
|
institution['text'] = '%(uaiLabel)s' % institution
|
|
institution['lat'] = institution.get('customLocationY') or institution.get('locationY')
|
|
institution['lon'] = institution.get('customLocationX') or institution.get('locationX')
|
|
institutions.append(institution)
|
|
institutions.sort(key=lambda x: x['text'])
|
|
return institutions
|
|
|
|
@endpoint(
|
|
perm='OPEN',
|
|
parameters={
|
|
'insee': {'description': _('INSEE City code'), 'example_value': '36005'},
|
|
},
|
|
)
|
|
def institutions(self, request, insee=None):
|
|
if insee:
|
|
query = '?' + urlencode({'inseeCode': insee})
|
|
else:
|
|
query = ''
|
|
return {'data': self.get_institutions(query)}
|
|
|
|
@endpoint(name='institutions', pattern=r'^from-city/(?P<city_insee_code>\d+)/*$', perm='OPEN')
|
|
def institutions_from_city(self, request, city_insee_code):
|
|
return {'data': self.get_institutions('/subscriberCity/%s' % city_insee_code)}
|
|
|
|
@endpoint(
|
|
name='search',
|
|
perm='can_access',
|
|
description=_('Get stop points based on a starting position and an arrival institution (API 2020)'),
|
|
parameters={
|
|
'lat': {'description': _('Latitude (departure)'), 'example_value': '46.828652'},
|
|
'lon': {'description': _('Longitude (departure)'), 'example_value': '1.701463'},
|
|
'address': {'description': _('Address (departure)')},
|
|
'institution': {'description': _('Institution ID (arrival)'), 'example_value': '277'},
|
|
'mode': {'description': _('Search mode: CLOSE_SCHOLAR (default) = 3km, FAR_ALL = 15km')},
|
|
},
|
|
)
|
|
def search(self, request, lat, lon, institution, address='', mode='CLOSE_SCHOLAR'):
|
|
payload = {
|
|
'from-lat': lat.replace(',', '.'),
|
|
'from-long': lon.replace(',', '.'),
|
|
'from-address': address,
|
|
'institution-id': institution,
|
|
'type': mode,
|
|
}
|
|
stops = self.request('wishes/search', payload)
|
|
for stop in stops:
|
|
stop['id'] = '%s' % stop['id']
|
|
stop['text'] = stop['commercial_name']
|
|
stop['lat'] = stop.get('latitude')
|
|
stop['lon'] = stop.get('longitude')
|
|
return {'data': stops}
|
|
|
|
@endpoint(
|
|
name='stop-areas',
|
|
pattern=r'^from-city/(?P<city_insee_code>\d+)/to-institution/(?P<institution_id>\d+)/*$',
|
|
perm='OPEN',
|
|
)
|
|
def stop_areas(self, request, city_insee_code, institution_id):
|
|
stops = self.request(
|
|
'stop-areas/subscriberCity/%s/institution/%s' % (city_insee_code, institution_id)
|
|
)
|
|
for stop in stops:
|
|
stop['id'] = '%s' % stop['id']
|
|
stop['text'] = stop['commercial_name']
|
|
return {'data': stops}
|
|
|
|
def get_ods(self, endpoint=''):
|
|
# ods = origin/destinations
|
|
okina_ods = self.request('ods' + endpoint)
|
|
ods = []
|
|
for okina_od in okina_ods:
|
|
base_id = 'inst:%(institutionId)s-seq:%(sequenceNumber)s' % okina_od['id']
|
|
for journey in okina_od['originDestinationSequenceElements']:
|
|
identifier = journey['okinaVehicleJourney']['publishedJourneyIdentifier']
|
|
if journey['okinaVehicleJourney']['okinaJourneyPattern']:
|
|
text = journey['okinaVehicleJourney']['okinaJourneyPattern']['publishedName']
|
|
else:
|
|
text = identifier
|
|
ods.append(
|
|
{
|
|
'id': '%s-%s-%s' % (base_id, journey['id'], journey['okinaVehicleJourney']['id']),
|
|
'text': text,
|
|
'vehicle_journey_id': '%s' % journey['okinaVehicleJourney']['id'],
|
|
'object_id': journey['okinaVehicleJourney']['objectId'],
|
|
'identifier': identifier,
|
|
}
|
|
)
|
|
return {'data': ods}
|
|
|
|
@endpoint(name='origin-destinations', perm='OPEN')
|
|
def origin_destinations(self, request):
|
|
return self.get_ods()
|
|
|
|
@endpoint(name='origin-destinations', pattern=r'^to-institution/(?P<institution_id>\d+)/*$', perm='OPEN')
|
|
def origin_destinations_to_institution(self, request, institution_id):
|
|
return self.get_ods('/institution/%s' % institution_id)
|
|
|
|
@endpoint(
|
|
name='origin-destinations',
|
|
pattern=r'^from-stop-area/(?P<stop_area_id>\d+)/to-institution/(?P<institution_id>\d+)/*$',
|
|
perm='OPEN',
|
|
)
|
|
def origin_destinations_from_stop_to_institution(self, request, stop_area_id, institution_id):
|
|
endpoint = 'ods/institution/%s/stop-area/%s' % (institution_id, stop_area_id)
|
|
okina_journeys = self.request(endpoint)
|
|
journeys = []
|
|
for n, okina_journey in enumerate(okina_journeys, 1):
|
|
journey = {
|
|
'id': str(n),
|
|
'text': ' + '.join(line['name'] for line in okina_journey),
|
|
'lines': [
|
|
{
|
|
'id': str(line['id']),
|
|
'text': line['name'],
|
|
}
|
|
for line in okina_journey
|
|
],
|
|
}
|
|
journeys.append(journey)
|
|
return {'data': journeys}
|
|
|
|
@endpoint(
|
|
name='origin-destinations',
|
|
pattern=r'^from-city/(?P<city_insee_code>\d+)/to-institution/(?P<institution_id>\d+)/*$',
|
|
perm='OPEN',
|
|
)
|
|
def origin_destinations_from_city_to_institution(self, request, city_insee_code, institution_id):
|
|
return self.get_ods('/institution/%s/subscriberCity/%s' % (institution_id, city_insee_code))
|
|
|
|
@endpoint(name='origin-destinations', pattern=r'^from-city/(?P<city_insee_code>\d+)/*$', perm='OPEN')
|
|
def origin_destinations_from_city(self, request, city_insee_code):
|
|
return self.get_ods('/subscriberCity/%s' % city_insee_code)
|
|
|
|
@endpoint(name='topology', pattern='^(?P<kind>(lines|networks|vehicle-journeys))/*$', perm='OPEN')
|
|
def topology(self, request, kind):
|
|
return {
|
|
'data': [
|
|
{'id': '%s' % item['id'], 'text': item['name']} for item in self.request('topology/%s' % kind)
|
|
]
|
|
}
|
|
|
|
@endpoint(name='subscriber', methods=['post'], perm='can_access')
|
|
def create_subscriber(self, request):
|
|
try:
|
|
payload = json.loads(request.body)
|
|
except ValueError:
|
|
raise APIError('payload must be a JSON object', http_status=400)
|
|
if not isinstance(payload, dict):
|
|
raise APIError('payload must be a dict', http_status=400)
|
|
return {'data': self.request('subscribers', payload, result_is_list=False)}
|
|
|
|
@endpoint(name='subscriber', pattern=r'^(?P<subscriber_id>\d+)/*$', methods=['get'], perm='can_access')
|
|
def get_subscriber(self, request, subscriber_id):
|
|
return {'data': self.request('subscribers/%s' % subscriber_id, result_is_list=False)}
|
|
|
|
@endpoint(name='subscriber', pattern=r'^(?P<subscriber_id>\d+)/qrcode/*$', perm='can_access')
|
|
def get_subscriber_qrcode(self, request, subscriber_id):
|
|
qrcode = self.request('subscribers/%s/qrcode' % subscriber_id, result_is_json=False)
|
|
content_type = qrcode.headers.get('Content-Type')
|
|
if not (content_type and content_type.startswith('image/')):
|
|
response = json.loads(qrcode.content)
|
|
raise APIError(response['message'], http_status=response['status'], err=response['code'])
|
|
return HttpResponse(qrcode.content, content_type=content_type)
|
|
|
|
@endpoint(name='subscription', methods=['post'], perm='can_access')
|
|
def create_subscription(self, request):
|
|
try:
|
|
payload = json.loads(request.body)
|
|
except ValueError:
|
|
raise APIError('payload must be a JSON object', http_status=400)
|
|
if not isinstance(payload, dict):
|
|
raise APIError('payload must be a dict', http_status=400)
|
|
if 'lineIds' in payload:
|
|
lineids = payload.pop('lineIds')
|
|
lineids = [lid.strip() for lid in lineids.split(',')]
|
|
subscriptions = []
|
|
for lineid in lineids:
|
|
payload['lineId'] = lineid
|
|
subscriptions.append(self.request('subscriptions', payload, result_is_list=False))
|
|
return {'data': subscriptions}
|
|
return {'data': self.request('subscriptions', payload, result_is_list=False)}
|
|
|
|
@endpoint(
|
|
name='subscription', pattern=r'^(?P<subscription_id>\d+)/*$', methods=['get'], perm='can_access'
|
|
)
|
|
def get_subscription(self, request, subscription_id):
|
|
return {'data': self.request('subscriptions/%s' % subscription_id, result_is_list=False)}
|