504 lines
19 KiB
Python
504 lines
19 KiB
Python
# 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 base64
|
|
from collections import OrderedDict
|
|
from urllib import parse as urlparse
|
|
|
|
from django.core.cache import cache
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
from requests.exceptions import ConnectionError
|
|
|
|
from passerelle.base.models import BaseResource, HTTPResource
|
|
from passerelle.utils.api import endpoint
|
|
from passerelle.utils.jsonresponse import APIError
|
|
|
|
DEMAND_SCHEMA = {
|
|
'$schema': 'http://json-schema.org/draft-04/schema#',
|
|
'title': 'Intervention demand',
|
|
'description': '',
|
|
'type': 'object',
|
|
'required': ['company', 'service', 'subject', 'name'],
|
|
'properties': OrderedDict(
|
|
{
|
|
'company': {
|
|
'description': 'Company code (sgesdemSoc)',
|
|
'type': 'string',
|
|
},
|
|
'service': {
|
|
'description': 'Service code (sgesdemSserv)',
|
|
'type': 'string',
|
|
},
|
|
'label': {
|
|
'description': 'Defined label code, required if LIBELDEMDEF=O (sgesdemLibdef)',
|
|
'type': 'string',
|
|
},
|
|
'subject': {
|
|
'description': 'Subject. Use defined label text if LIBELDEMDEF=O (sgesdemNdt)',
|
|
'type': 'string',
|
|
},
|
|
'description': {
|
|
'description': 'Description, up to 4000 chars (sgesdemLibelle)',
|
|
'type': 'string',
|
|
},
|
|
'name': {
|
|
'description': 'Applicant first name and last name (sgesdemCplnom)',
|
|
'type': 'string',
|
|
},
|
|
'email': {
|
|
'description': 'Email address (sgesdemCplemail)',
|
|
'type': 'string',
|
|
},
|
|
'phone1': {
|
|
'description': 'Phone 1 (sgesdemCpltel1)',
|
|
'type': 'string',
|
|
},
|
|
'phone2': {
|
|
'description': 'Phone 2 (sgesdemCpltel2)',
|
|
'type': 'string',
|
|
},
|
|
'address1': {
|
|
'description': 'Address, first line (sgesdemCpladr1)',
|
|
'type': 'string',
|
|
},
|
|
'address2': {
|
|
'description': 'Address, second line (sgesdemCpladr2)',
|
|
'type': 'string',
|
|
},
|
|
'address3': {
|
|
'description': 'Address, third line (sgesdemCpladr2)',
|
|
'type': 'string',
|
|
},
|
|
}
|
|
),
|
|
}
|
|
|
|
ADD_DOCUMENT_SCHEMA = {
|
|
'$schema': 'http://json-schema.org/draft-04/schema#',
|
|
'title': 'Intervention demand',
|
|
'description': '',
|
|
'type': 'object',
|
|
'required': ['demand_id', 'document', 'title'],
|
|
'properties': OrderedDict(
|
|
{
|
|
'demand_id': {
|
|
'description': 'Document title (docTitre)',
|
|
'type': 'string',
|
|
},
|
|
'title': {
|
|
'description': 'Document title (docTitre)',
|
|
'type': 'string',
|
|
},
|
|
'filename': {
|
|
'description': 'Document filename (docFile)',
|
|
'type': 'string',
|
|
},
|
|
'document': {
|
|
'type': 'object',
|
|
'description': 'File object (file0)',
|
|
'required': ['filename', 'content_type', 'content'],
|
|
'properties': {
|
|
'filename': {
|
|
'type': 'string',
|
|
},
|
|
'content_type': {
|
|
'type': 'string',
|
|
'description': 'MIME content-type',
|
|
},
|
|
'content': {
|
|
'type': 'string',
|
|
'description': 'Content, base64 encoded',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
),
|
|
}
|
|
|
|
|
|
class ASTech(BaseResource, HTTPResource):
|
|
category = _('Business Process Connectors')
|
|
|
|
base_url = models.URLField(
|
|
verbose_name=_('Webservice Base URL'),
|
|
help_text=_('Base API URL (example: https://astech-symphonie.com/app.php/)'),
|
|
)
|
|
connection = models.CharField(
|
|
verbose_name=_('Connection code'),
|
|
help_text=_('See possibilities with "connections" endpoint. If empty, default code is used'),
|
|
max_length=64,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
|
|
_category_ordering = [_('Parameters'), _('Rules'), _('Demand'), 'Tech & Debug']
|
|
|
|
class Meta:
|
|
verbose_name = _('AS-TECH')
|
|
|
|
def call_json(self, method, url, **kwargs):
|
|
try:
|
|
response = self.requests.request(method, url, timeout=25, **kwargs)
|
|
except ConnectionError as e:
|
|
raise APIError('connection error: %s' % e)
|
|
if response.status_code not in (200, 201):
|
|
try:
|
|
content = response.json()
|
|
except ValueError:
|
|
content = response.content[:1024]
|
|
raise APIError(
|
|
'AS-TECH response: %s %s' % (response.status_code, response.reason),
|
|
data={
|
|
'error': {
|
|
'status': response.status_code,
|
|
'reason': response.reason,
|
|
'content': content,
|
|
}
|
|
},
|
|
)
|
|
try:
|
|
json_data = response.json()
|
|
except ValueError:
|
|
raise APIError('invalid JSON in response: %r' % response.content[:1024])
|
|
return json_data
|
|
|
|
def get_connections(self):
|
|
url = urlparse.urljoin(self.base_url, 'connection/all')
|
|
connections = self.call_json('get', url)
|
|
if self.connection:
|
|
if self.connection not in connections['connections']:
|
|
raise APIError(
|
|
'connection %s not found in available connections: %r' % (self.connection, connections)
|
|
)
|
|
connections['default'] = self.connection
|
|
connections['connection'] = connections['connections'][connections['default']]
|
|
return connections
|
|
|
|
def get_authorization(self):
|
|
cache_key = 'astech-%s-%s-authorization' % (self.connection or 'default', self.id)
|
|
authorization = cache.get(cache_key)
|
|
if authorization:
|
|
return authorization
|
|
connection = self.get_connections()['connection']
|
|
params = {
|
|
'client_id': connection['clientId'],
|
|
'connection_id': connection['code'],
|
|
'format': 'json',
|
|
'response_type': 'token',
|
|
}
|
|
url = urlparse.urljoin(self.base_url, 'oauth/v2/auth')
|
|
authorization = self.call_json('post', url, params=params, json={'application': 'interfaceCitoyenne'})
|
|
timeout = min(120, authorization['expires_in']) # do not trust expires_in delay (1 day in tests)
|
|
cache.set(cache_key, authorization, timeout)
|
|
return authorization
|
|
|
|
def call(self, endpoint, **kwargs):
|
|
url = urlparse.urljoin(self.base_url, endpoint)
|
|
params = kwargs.pop('params', {})
|
|
json = kwargs.pop('json', None)
|
|
method = kwargs.pop('method', 'get')
|
|
authorization = self.get_authorization()
|
|
params['access_token'] = authorization['access_token']
|
|
params['connection_id'] = authorization['connection_id']
|
|
if json is not None:
|
|
json_response = self.call_json('post', url, params=params, json=json, **kwargs)
|
|
else:
|
|
json_response = self.call_json(method, url, params=params, **kwargs)
|
|
return json_response
|
|
|
|
@endpoint(
|
|
name='connections',
|
|
description=_('See all possible connections codes (see configuration)'),
|
|
display_category='Tech & Debug',
|
|
display_order=1,
|
|
)
|
|
def connections(self, request):
|
|
return {'data': self.get_connections()}
|
|
|
|
@endpoint(
|
|
name='authorization',
|
|
description=_('See authorization tokens (testing only)'),
|
|
display_category='Tech & Debug',
|
|
display_order=2,
|
|
)
|
|
def authorization(self, request):
|
|
return {'data': self.get_authorization()}
|
|
|
|
@endpoint(
|
|
name='services',
|
|
description=_('List authorized services for connected user'),
|
|
display_category=_('Rules'),
|
|
display_order=1,
|
|
)
|
|
def services(self, request):
|
|
services = self.call('apicli/rule-call-by-alias/sousservices/invoke', method='post')
|
|
services = [{'id': key, 'text': value} for key, value in services.items()]
|
|
services.sort(key=lambda item: item['id']) # "same as output" sort
|
|
return {'data': services}
|
|
|
|
@endpoint(
|
|
name='company',
|
|
description=_('Company code of the applicant'),
|
|
parameters={
|
|
'applicant': {
|
|
'description': _(
|
|
'Applicant code (codeDemandeur). If absent, use HTTP basic authentication username'
|
|
)
|
|
},
|
|
},
|
|
display_category=_('Rules'),
|
|
display_order=2,
|
|
)
|
|
def company(self, request, applicant=None):
|
|
if applicant is None:
|
|
applicant = self.basic_auth_username
|
|
company = self.call(
|
|
'apicli/rule-call-by-alias/code_societe_demandeur/invoke', json={'codeDemandeur': applicant}
|
|
)
|
|
return {'data': {'company': company}}
|
|
|
|
@endpoint(
|
|
name='companies',
|
|
description=_('List of authorized companies for an applicant'),
|
|
parameters={
|
|
'applicant': {
|
|
'description': _(
|
|
'Applicant code (codeDemandeur). If absent, use HTTP basic authentication username'
|
|
)
|
|
},
|
|
'signatory': {'description': _('Signatory code (codeSignataire), supersede applicant')},
|
|
'kind': {
|
|
'description': _('Kind(s) of request: 1=parts, 2=interventions, 3=loans'),
|
|
'example_value': '1,3',
|
|
},
|
|
},
|
|
display_category=_('Rules'),
|
|
display_order=3,
|
|
)
|
|
def companies(self, request, applicant=None, signatory=None, kind='1,2,3'):
|
|
if applicant is None and signatory is None:
|
|
applicant = self.basic_auth_username
|
|
kind = ','.join([item.strip() for item in kind.split(',') if item.strip()])
|
|
payload = {
|
|
'codeDemandeur': applicant or '',
|
|
'codeSignataire': signatory or '',
|
|
'typeDemande': kind,
|
|
'designation': True,
|
|
}
|
|
companies = self.call('apicli/rule-call-by-alias/societes_demandeur/invoke', json=payload)
|
|
if not isinstance(companies, dict):
|
|
raise APIError('Invalid response: %s' % companies)
|
|
companies = [{'id': str(key), 'text': value} for key, value in companies.items()]
|
|
companies.sort(key=lambda item: item['id']) # "same as output" sort
|
|
return {'data': companies}
|
|
|
|
@endpoint(
|
|
name='labels',
|
|
description=_('List of predefined labels for a company'),
|
|
parameters={
|
|
'company': {
|
|
'description': _('Company code (societeDemandeur). If absent, use "company" endpoint result')
|
|
},
|
|
},
|
|
display_category=_('Rules'),
|
|
display_order=4,
|
|
)
|
|
def labels(self, request, company=None):
|
|
if company is None:
|
|
company = self.company(request)['data']['company']
|
|
labels = self.call(
|
|
'apicli/rule-call-by-alias/libelles_predefinis/invoke', json={'societeDemandeur': company}
|
|
)
|
|
if not isinstance(labels, dict):
|
|
raise APIError('Invalid response: %s' % labels)
|
|
labels = [{'id': str(key), 'text': value} for key, value in labels.items()]
|
|
labels.sort(key=lambda item: item['id']) # "same as output" sort
|
|
return {'data': labels}
|
|
|
|
@endpoint(
|
|
name='parameter',
|
|
description=_('Value of a parameter'),
|
|
parameters={
|
|
'name': {'description': _('Name of the parameter'), 'example_value': 'LIBELDEMDEF'},
|
|
'company': {'description': _('Company code. If absent, use "company" endpoint result')},
|
|
},
|
|
display_category=_('Parameters'),
|
|
display_order=5,
|
|
)
|
|
def parameter(self, request, name, company=None):
|
|
if company is None:
|
|
company = self.company(request)['data']['company']
|
|
endpoint = 'apicli/common/getparam/' + name
|
|
if company:
|
|
endpoint += '/' + company
|
|
value = self.call(endpoint)
|
|
return {'data': value}
|
|
|
|
@endpoint(
|
|
name='create-demand',
|
|
description=_('Create a demand'),
|
|
methods=['post'],
|
|
post={'request_body': {'schema': {'application/json': DEMAND_SCHEMA}}},
|
|
display_category=_('Demand'),
|
|
display_order=1,
|
|
)
|
|
def create_demand(self, request, post_data):
|
|
payload = {
|
|
'interface_citoyenne_demande': {
|
|
'sgesdemSoc': post_data['company'],
|
|
'sgesdemSserv': post_data['service'],
|
|
'sgesdemLibdef': post_data.get('label') or '',
|
|
'sgesdemNdt': post_data['subject'],
|
|
'sgesdemLibelle': post_data.get('description') or '',
|
|
'sgesdemCplnom': post_data['name'] or '',
|
|
'sgesdemCplemail': post_data.get('email') or '',
|
|
'sgesdemCpltel1': post_data.get('phone1') or '',
|
|
'sgesdemCpltel2': post_data.get('phone2') or '',
|
|
'sgesdemCpladr1': post_data.get('address1') or '',
|
|
'sgesdemCpladr2': post_data.get('address2') or '',
|
|
'sgesdemCpladr3': post_data.get('address3') or '',
|
|
}
|
|
}
|
|
|
|
# forward payload parameters matching APIs params name pattern
|
|
for key, value in post_data.items():
|
|
if key.startswith('sgesdem'):
|
|
payload['interface_citoyenne_demande'][key] = value
|
|
|
|
create = self.call('apicli/interface-citoyenne/demande-intervention', json=payload)
|
|
if not isinstance(create, dict) or not create.get('sgesdemNum'):
|
|
raise APIError('no sgesdemNum in response: %s' % create)
|
|
create['demand_id'] = create['sgesdemNum']
|
|
return {'data': create}
|
|
|
|
@endpoint(
|
|
name='add-document',
|
|
description=_('Add a document in a demand'),
|
|
methods=['post'],
|
|
post={'request_body': {'schema': {'application/json': ADD_DOCUMENT_SCHEMA}}},
|
|
display_category=_('Demand'),
|
|
display_order=2,
|
|
)
|
|
def add_document(self, request, post_data):
|
|
endpoint = 'apicli/interface-citoyenne/document/sgesdemNum/' + post_data['demand_id']
|
|
filename = post_data.get('filename') or post_data['document']['filename']
|
|
params = {
|
|
'docTitre': post_data['title'],
|
|
'docFile': filename,
|
|
}
|
|
content = base64.b64decode(post_data['document']['content'])
|
|
content_type = post_data['document']['content_type']
|
|
files = {'file0': (filename, content, content_type)}
|
|
added = self.call(endpoint, method='post', params=params, files=files)
|
|
return {'data': added}
|
|
|
|
@endpoint(
|
|
name='demand-position',
|
|
description=_('Get demand position'),
|
|
parameters={
|
|
'demand_id': {
|
|
'description': _('Demand id'),
|
|
'example_value': '000000000001234',
|
|
},
|
|
},
|
|
display_category=_('Demand'),
|
|
display_order=3,
|
|
)
|
|
def demand_position(self, request, demand_id):
|
|
endpoint = 'apicli/demande/position/' + demand_id
|
|
position = self.call(endpoint)
|
|
if not isinstance(position, dict) or not position.get('position'):
|
|
raise APIError('no position in response: %s' % position)
|
|
position['id'] = position['position']
|
|
position['text'] = position.get('positionLib')
|
|
return {'data': position}
|
|
|
|
@endpoint(
|
|
name='demand-all-positions',
|
|
description=_('List all demand possible positions'),
|
|
display_category=_('Demand'),
|
|
display_order=4,
|
|
)
|
|
def demand_all_positions(self, request):
|
|
endpoint = 'apicli/demande/positions'
|
|
positions = self.call(endpoint)
|
|
for position in positions:
|
|
position['id'] = position['position']
|
|
position['text'] = position['positionLib']
|
|
return {'data': positions}
|
|
|
|
@endpoint(
|
|
name='list-views',
|
|
display_order=1,
|
|
description=_('List available views'),
|
|
display_category=_('Referential'),
|
|
)
|
|
def list_views(self, request):
|
|
results = self.call('apicli/data/views')
|
|
astech_views = results.get('views', [])
|
|
for view in astech_views:
|
|
view['id'] = view['apivId']
|
|
view['text'] = view['apivNom']
|
|
return {'data': astech_views}
|
|
|
|
@endpoint(
|
|
name='get-view-columns',
|
|
display_order=2,
|
|
description=_('Get view columns'),
|
|
display_category=_('Referential'),
|
|
parameters={
|
|
'code': {
|
|
'description': _('View code'),
|
|
'example_value': 'ASTECH_BIENS',
|
|
},
|
|
},
|
|
)
|
|
def get_view_columns(self, request, code):
|
|
endpoint = 'apicli/data/%s/columns' % code
|
|
results = self.call(endpoint)
|
|
columns = results.get('columns', [])
|
|
for column in columns:
|
|
column['id'] = column['code']
|
|
column['text'] = column['des']
|
|
return {'data': columns}
|
|
|
|
@endpoint(
|
|
name='get-view-data',
|
|
display_order=3,
|
|
description=_('Get view data'),
|
|
display_category=_('Referential'),
|
|
datasource=True,
|
|
parameters={
|
|
'code': {
|
|
'description': _('View code'),
|
|
'example_value': 'ASTECH_BIENS',
|
|
},
|
|
'id_column': {'description': _('Name of column contaning the id'), 'example_value': 'BIEN_ID'},
|
|
'text_column': {
|
|
'description': _('Name of column contaning the label'),
|
|
'example_value': 'DESIGNATION',
|
|
},
|
|
},
|
|
)
|
|
def get_view_data(self, request, code, id_column, text_column):
|
|
endpoint = 'apicli/data/%s/results' % code
|
|
results = self.call(endpoint, json={'data': {'filters': []}})
|
|
for result in results:
|
|
result['id'] = result[id_column]
|
|
result['text'] = result[text_column]
|
|
return {'data': results}
|