275 lines
10 KiB
Python
275 lines
10 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 datetime
|
|
import json
|
|
import re
|
|
|
|
from django.db import models
|
|
from django.utils.timezone import get_fixed_timezone, utc, is_naive, make_aware
|
|
from django.utils.dateparse import parse_date, parse_datetime
|
|
from django.utils.six import string_types
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
from passerelle.base.models import BaseResource
|
|
from passerelle.compat import json_loads
|
|
from passerelle.utils.api import endpoint
|
|
from passerelle.utils.jsonresponse import APIError
|
|
|
|
# Only for documentation
|
|
REFERENTIALS = ('service', 'typology', 'inputchannel', 'structure', 'quartierelu',
|
|
'secteurterritoriale', 'civility', 'title')
|
|
|
|
|
|
# GDEMA date format is /Date(1510786800000+0100)/ (tz is optionnal)
|
|
gdema_datetime_re = re.compile(
|
|
r'/Date\('
|
|
r'(?P<timestamp_ms>\d+)'
|
|
r'(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?'
|
|
r'\)/$'
|
|
)
|
|
|
|
|
|
def parse_gdema_datetime(value):
|
|
match = gdema_datetime_re.match(value)
|
|
if match:
|
|
kw = match.groupdict()
|
|
timestamp = int(kw['timestamp_ms'])/1000.0
|
|
tzinfo = kw.get('tzinfo')
|
|
if tzinfo == 'Z':
|
|
tzinfo = utc
|
|
elif tzinfo is not None:
|
|
offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
|
|
offset = 60 * int(tzinfo[1:3]) + offset_mins
|
|
if tzinfo[0] == '-':
|
|
offset = -offset
|
|
tzinfo = get_fixed_timezone(offset)
|
|
return datetime.datetime.fromtimestamp(timestamp, tzinfo)
|
|
return None
|
|
|
|
|
|
def normalize(value):
|
|
'''convert /Date()/ to datetime, integers to strings'''
|
|
if isinstance(value, string_types):
|
|
datetime = parse_gdema_datetime(value)
|
|
if datetime:
|
|
return datetime
|
|
else:
|
|
return value
|
|
if isinstance(value, bool):
|
|
return value
|
|
if isinstance(value, int):
|
|
return '%d' % value
|
|
if isinstance(value, list):
|
|
return [normalize(item) for item in value]
|
|
if isinstance(value, dict):
|
|
return {k: normalize(v) for k, v in value.items()}
|
|
return value
|
|
|
|
|
|
def gdema_datetime(value):
|
|
if not value:
|
|
return None
|
|
dt = parse_datetime(value)
|
|
if not dt:
|
|
dt = parse_date(value)
|
|
if not dt:
|
|
return None
|
|
dt = datetime.datetime(dt.year, dt.month, dt.day)
|
|
if is_naive(dt):
|
|
dt = make_aware(dt)
|
|
timestamp_ms = (dt-datetime.datetime(1970, 1, 1, tzinfo=utc)).total_seconds() * 1000
|
|
tzinfo = dt.strftime('%z')
|
|
return '/Date(%d%s)/' % (timestamp_ms, tzinfo)
|
|
|
|
|
|
def to_gdema(input_dict):
|
|
'''
|
|
nameDate: ... -> nameDate: /Date(...)/
|
|
name: {publik file dict} -> name: {gdema file dict}
|
|
Name_Key: value -> Name: {Key: value, ...}
|
|
Name_<int>: value -> Name: [value, ...]
|
|
'''
|
|
gdema_dict = {}
|
|
for key, value in input_dict.items():
|
|
# nameDate: ... -> nameDate: /Date(...)/
|
|
if key.endswith('Date'):
|
|
value = gdema_datetime(value)
|
|
# name: {publik file dict} -> name: {gdema file dict}
|
|
if isinstance(value, dict) and ('filename' in value and
|
|
'content' in value):
|
|
value = {
|
|
'Name': value['filename'],
|
|
'Base64Stream': value['content'],
|
|
}
|
|
# Name_Key: value -> Name: {Key: value, ...}
|
|
# Name_1: value -> Name: [value, ...] (first element index is 1)
|
|
if '_' in key:
|
|
key, index = key.split('_')
|
|
try:
|
|
index = int(index)
|
|
except ValueError:
|
|
pass
|
|
if isinstance(index, int):
|
|
# update/create a list
|
|
if key not in gdema_dict:
|
|
gdema_dict[key] = []
|
|
if len(gdema_dict[key]) >= index:
|
|
gdema_dict[key][index-1] = value
|
|
else:
|
|
holes = [None for i in range(index-len(gdema_dict[key])-1)]
|
|
gdema_dict[key].extend(holes)
|
|
gdema_dict[key].append(value)
|
|
else:
|
|
# index is not a integer, update/create a dict
|
|
if key in gdema_dict:
|
|
gdema_dict[key][index] = value
|
|
else:
|
|
gdema_dict[key] = {index: value}
|
|
else:
|
|
gdema_dict[key] = value
|
|
return gdema_dict
|
|
|
|
|
|
class Gdema(BaseResource):
|
|
service_url = models.URLField(max_length=256, blank=False,
|
|
verbose_name=_('Service URL'),
|
|
help_text=_('GDEMA API base URL'))
|
|
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'))
|
|
password = models.CharField(max_length=128, blank=True, verbose_name=_('Password'))
|
|
|
|
category = _('Business Process Connectors')
|
|
|
|
class Meta:
|
|
verbose_name = _('GDEMA (Strasbourg requests and interventions system)')
|
|
|
|
def request(self, endpoint, payload=None):
|
|
url = self.service_url + endpoint
|
|
if self.username or self.password:
|
|
auth = (self.username, self.password)
|
|
else:
|
|
auth = None
|
|
headers = {}
|
|
headers['Accept'] = 'application/json'
|
|
if payload is None:
|
|
result = self.requests.get(url, auth=auth, headers=headers)
|
|
else:
|
|
headers['Content-Type'] = 'application/json'
|
|
data = json.dumps(payload)
|
|
result = self.requests.post(url, data=data, auth=auth, headers=headers)
|
|
if result.status_code < 200 or result.status_code >= 300:
|
|
raise APIError('GDEMA returns HTTP status %s' % result.status_code)
|
|
return result.json()
|
|
|
|
def get_services(self):
|
|
services = self.request('referentiel/service')
|
|
for service in services:
|
|
service['id'] = '%s' % service['Id']
|
|
service['text'] = service['Label']
|
|
del service['Id']
|
|
del service['Label']
|
|
del service['Typology']
|
|
return services
|
|
|
|
def get_typologies(self, service_id=None):
|
|
services = self.request('referentiel/service')
|
|
typologies = []
|
|
for service in services:
|
|
if not service_id or service['Id'] == service_id:
|
|
for typology in service['Typology']:
|
|
typologies.append({
|
|
'id': '%s' % typology['Value'],
|
|
'text': typology['Text'],
|
|
'service_id': '%s' % service['Id'],
|
|
'service_text': service['Label'],
|
|
})
|
|
if service_id:
|
|
break
|
|
return typologies
|
|
|
|
def check_status(self):
|
|
self.get_services()
|
|
|
|
@endpoint(name='referentiel', pattern='^(?P<name>\w+)/*$',
|
|
description=_('Get reference items'),
|
|
example_pattern='{name}/',
|
|
parameters={
|
|
'name': {
|
|
'description': _('Referential name: (%s)') % ' | '.join(REFERENTIALS),
|
|
'example_value': 'inputchannel',
|
|
},
|
|
'service_id': {
|
|
'description': _('Filter by service id (for typology referential)'),
|
|
'example_value': '21714',
|
|
},
|
|
},
|
|
methods=['get'], perm='can_access')
|
|
def referentiel(self, request, name, service_id=None):
|
|
if name == 'service':
|
|
return {'data': self.get_services()}
|
|
if name == 'typology':
|
|
return {'data': self.get_typologies(service_id)}
|
|
data = []
|
|
items = self.request('referentiel/%s' % name)
|
|
for item in items:
|
|
data.append({
|
|
'id': '%s' % item['Value'],
|
|
'text': item['Text'],
|
|
})
|
|
return {'data': data}
|
|
|
|
@endpoint(name='create-request',
|
|
description=_('Create a new request (POST)'),
|
|
methods=['post'], perm='can_access')
|
|
def create_request(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)
|
|
payload = to_gdema(payload)
|
|
data = self.request('request/create', payload)
|
|
return {'data': normalize(data)}
|
|
|
|
@endpoint(name='get-request', pattern='^(?P<request_id>\d+)/*$',
|
|
description=_('Get request details'),
|
|
example_pattern='{request_id}/',
|
|
parameters={
|
|
'request_id': {
|
|
'description': _('Request Id'),
|
|
'example_value': '10',
|
|
},
|
|
},
|
|
methods=['get'], perm='can_access')
|
|
def get_request(self, request, request_id):
|
|
data = self.request('request/%s' % request_id)
|
|
return {'data': normalize(data)}
|
|
|
|
@endpoint(name='get-request-state', pattern='^(?P<request_id>\d+)/*$',
|
|
description=_('Get request status'),
|
|
example_pattern='{request_id}/',
|
|
parameters={
|
|
'request_id': {
|
|
'description': _('Request Id'),
|
|
'example_value': '10',
|
|
},
|
|
},
|
|
methods=['get'], perm='can_access')
|
|
def get_request_state(self, request, request_id):
|
|
data = self.request('request/%s/state' % request_id)
|
|
return {'data': normalize(data)}
|