passerelle/passerelle/apps/okina/models.py

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)}