passerelle/passerelle/apps/astech/models.py

450 lines
17 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)'),
perm='can_access',
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)'),
perm='can_access',
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"),
perm='can_access',
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'),
perm='can_access',
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'),
perm='can_access',
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)
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'),
perm='can_access',
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}
)
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"),
perm='can_access',
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'),
perm='can_access',
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'),
perm='can_access',
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'),
perm='can_access',
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'),
perm='can_access',
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}