passerelle/passerelle/contrib/gdema/models.py

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