passerelle/passerelle/apps/atal_rest/models.py

498 lines
16 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2023 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
import binascii
import collections
import io
import json
import urllib
import requests
from django.db import models
from django.utils import dateparse
from django.utils.translation import gettext_lazy as _
from passerelle.base.models import BaseResource, HTTPResource
from passerelle.utils.api import endpoint
from passerelle.utils.jsonresponse import APIError
FILE_OBJECT = {
'type': 'object',
'description': 'File object',
'required': ['content'],
'properties': {
'filename': {
'type': 'string',
'description': 'Filename',
},
'content': {
'type': 'string',
'description': 'Content',
},
'content_type': {
'type': 'string',
'description': 'Content type',
},
},
}
SINGLE_ATTACHMENT_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'additionalProperties': False,
'properties': {
'file': {
'oneOf': [
FILE_OBJECT,
{'type': 'string', 'description': 'empty file, do not consider', 'pattern': r'^$'},
{'type': 'null', 'description': 'empty file, do not consider'},
]
}
},
'required': ['file'],
}
ATTACHMENTS_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'additionalProperties': False,
'properties': {
'files': {
'type': 'array',
'items': {
'oneOf': [
FILE_OBJECT,
{'type': 'string', 'description': 'empty file, do not consider', 'pattern': r'^$'},
{'type': 'null', 'description': 'empty file, do not consider'},
]
},
},
'worksrequests_ids': {'type': 'array', 'items': {'type': 'string'}},
},
'required': ['files', 'worksrequests_ids'],
'unflatten': True,
}
WORKSREQUESTS_SCHEMA = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties': collections.OrderedDict(
{
'activity_nature_id': {'type': 'string'},
'comments': {'type': 'string'},
'contact': {
'type': 'object',
'properties': {
'adress1': {'type': 'string'},
'city': {'type': 'string'},
'email': {'type': 'string'},
'first_name': {'type': 'string'},
'last_name': {'type': 'string'},
'mobile': {'type': 'string'},
'phone': {'type': 'string'},
'zipcode': {'type': 'string'},
},
},
'description': {'type': 'string'},
'desired_date': {'type': 'string', 'description': 'format YYYY-MM-DD'},
'keywords': {'type': 'string'},
'latitude': {'type': 'string'},
'localisation': {'type': 'string'},
'longitude': {'type': 'string'},
'object': {'type': 'string'},
'operator': {'type': 'string'},
'patrimony_id': {'type': 'string'},
'priority_id': {'type': 'string'},
'recipient_id': {'type': 'string'},
'request_date': {
'type': 'string',
'description': 'format YYYY-MM-DD',
},
'requester_id': {'type': 'string'},
'requesting_department_id': {'type': 'string'},
'request_type': {'type': 'string'},
'suggested_recipient_id': {'type': 'string'},
'thematic_ids': {'type': 'array', 'items': {'type': 'string'}},
}
),
'required': ['object', 'recipient_id', 'requester_id', 'requesting_department_id'],
'unflatten': True,
}
STATUS_MAP = {
0: 'En attente',
1: 'En analyse',
2: 'Acceptée',
3: 'Refusée',
4: 'Annulée',
5: 'Ajournée',
6: 'Brouillon',
7: 'Redirigée',
8: 'Prise en compte',
9: 'Clôturée',
13: 'Archivée',
14: 'À spécifier',
15: 'À valider',
}
INTERVENTION_STATUS_MAP = {
1: 'Pas commencé',
2: 'En cours',
4: 'Terminé',
5: 'Fermé',
}
def to_ds(record):
record['id'] = record['Id']
record['text'] = record['Name']
return record
class AtalREST(BaseResource, HTTPResource):
base_url = models.URLField(_('API URL'))
api_key = models.CharField(max_length=1024, verbose_name=_('API key'))
category = _('Business Process Connectors')
class Meta:
verbose_name = _('Atal REST')
def _call(
self, path, method='get', params=None, data=None, json_data=None, files=None, return_response=False
):
url = urllib.parse.urljoin(self.base_url, path)
kwargs = {}
kwargs['headers'] = {'X-API-Key': self.api_key}
if params:
kwargs['params'] = params
if method == 'post':
if not json_data:
json_data = {}
kwargs['json'] = json_data
if files:
kwargs['files'] = files
if data:
kwargs['data'] = data
try:
resp = self.requests.request(url=url, method=method, **kwargs)
except (requests.Timeout, requests.RequestException) as e:
raise APIError(str(e))
try:
resp.raise_for_status()
except requests.RequestException as main_exc:
try:
err_data = resp.json()
except (json.JSONDecodeError, requests.exceptions.RequestException):
err_data = {'response_text': resp.text}
raise APIError(str(main_exc), data=err_data)
if return_response:
return resp
try:
return resp.json()
except (json.JSONDecodeError, requests.exceptions.RequestException) as e:
raise APIError(str(e))
def check_status(self):
return self._call('api/Test', return_response=True)
@endpoint(
methods=['get'],
name='thirdparties-requesting-departments',
description=_('Get the third parties requesting departments referential'),
parameters={
'request_type': {
'example_value': '1001',
}
},
)
def thirdparties_requesting_departments(self, request, request_type):
return {
'data': [
to_ds(record)
for record in self._call(
'api/ThirdParties/RequestingDepartments', params={'RequestType': request_type}
)
]
}
@endpoint(
methods=['get'],
description=_('Get the users referential'),
)
def users(self, request):
return {'data': [to_ds(record) for record in self._call('api/Users')]}
@endpoint(
description=_('Create a works request'),
post={
'request_body': {
'schema': {
'application/json': WORKSREQUESTS_SCHEMA,
}
},
'input_example': {
'activity_nature_id': '0',
'comments': 'some comment',
'contact/adress1': '1 rue des cinq diamants',
'contact/city': 'paris',
'contact/email': 'foo@bar.invalid',
'contact/first_name': 'john',
'contact/last_name': 'doe',
'contact/mobile': '0606060606',
'contact/phone': '0101010101',
'contact/zipcode': '75013',
'description': 'some description',
'desired_date': '2023-06-28',
'keywords': 'foo bar',
'latitude': '0',
'localisation': 'somewhere',
'longitude': '0',
'object': 'some object',
'operator': 'some operator',
'patrimony_id': '0',
'priority_id': '0',
'recipient_id': '0',
'request_date': '2023-06-27',
'requester_id': '0',
'requesting_department_id': '0',
'request_type': '0',
'suggested_recipient_id': {'type': 'string'},
'thematic_ids/0': '1',
'thematic_ids/1': '2',
},
},
)
def worksrequests(self, request, post_data):
data = {}
int_params = {
'activity_nature_id': 'ActivityNatureId',
'latitude': 'Latitude',
'longitude': 'Longitude',
'patrimony_id': 'PatrimonyId',
'priority_id': 'PriorityId',
'recipient_id': 'RecipientId',
'requester_id': 'RequesterId',
'requesting_department_id': 'RequestingDepartmentId',
'request_type': 'RequestType',
'suggested_recipient_id': 'SuggestedRecipientId',
}
for param, atal_param in int_params.items():
if param in post_data:
try:
data[atal_param] = int(post_data[param])
except ValueError:
raise APIError('%s must be an integer' % param)
if 'thematic_ids' in post_data:
data['ThematicIds'] = []
for thematic_id in post_data['thematic_ids']:
try:
data['ThematicIds'].append(int(thematic_id))
except ValueError:
raise APIError('a thematic identifier must be an intenger')
datetime_params = {
'desired_date': 'DesiredDate',
'request_date': 'RequestDate',
}
for param, atal_param in datetime_params.items():
if param in post_data:
try:
obj = dateparse.parse_date(post_data[param])
except ValueError:
obj = None
if obj is None:
raise APIError(
'%s must be a valid YYYY-MM-DD date (received: "%s")' % (param, post_data[param])
)
data[atal_param] = obj.isoformat()
contact_params = {
'adress1': 'Adress1',
'city': 'City',
'email': 'Email',
'first_name': 'FirstName',
'last_name': 'LastName',
'mobile': 'Mobile',
'phone': 'Phone',
'zipcode': 'ZipCode',
}
if 'contact' in post_data:
data['Contact'] = {}
for param, atal_param in contact_params.items():
if param in post_data['contact']:
data['Contact'][atal_param] = post_data['contact'][param]
string_params = {
'comments': 'Comments',
'description': 'Description',
'keywords': 'Keywords',
'localisation': 'Localisation',
'object': 'Object',
'operator': 'Operator',
}
for param, atal_param in string_params.items():
if param in post_data:
data[atal_param] = post_data[param]
resp_data = self._call('api/WorksRequests', method='post', json_data=data)
resp_data['RequestStateLabel'] = STATUS_MAP.get(resp_data.get('RequestState', ''), '')
return {'data': resp_data}
@endpoint(
description=_('Add an attachment to a works requests'),
name='worksrequests-single-attachment',
post={
'request_body': {
'schema': {
'application/json': SINGLE_ATTACHMENT_SCHEMA,
}
},
'input_example': {
'file': {
'filename': 'example-1.pdf',
'content_type': 'application/pdf',
'content': 'JVBERi0xL...(base64 PDF)...',
},
},
},
parameters={
'worksrequests_id': {
'example_value': '1',
}
},
)
def worksrequests_single_attachment(self, request, worksrequests_id, post_data):
if not post_data['file']:
return {}
try:
content = base64.b64decode(post_data['file']['content'])
except (TypeError, binascii.Error):
raise APIError('Invalid file content')
files = {
'File': (
post_data['file'].get('filename', ''),
io.BytesIO(content).read(),
post_data['file'].get('content_type', ''),
)
}
# return nothing if successful
self._call(
'api/WorksRequests/%s/Attachments' % worksrequests_id,
method='post',
files=files,
return_response=True,
)
return {}
@endpoint(
description=_('Add attachments to multiple works requests'),
name='worksrequests-attachments',
post={
'request_body': {
'schema': {
'application/json': ATTACHMENTS_SCHEMA,
}
},
'input_example': {
'files/0': {
'filename': 'example-1.pdf',
'content_type': 'application/pdf',
'content': 'JVBERi0xL...(base64 PDF)...',
},
'files/1': {
'filename': 'example-2.pdf',
'content_type': 'application/pdf',
'content': 'JVBERi0xL...(base64 PDF)...',
},
'worksrequests_ids/0': '1',
'worksrequests_ids/1': '2',
},
},
)
def worksrequests_attachments(self, request, post_data):
files = []
for file_ in post_data.get('files', []):
if not file_:
continue
try:
content = base64.b64decode(file_['content'])
except (TypeError, binascii.Error):
raise APIError('Invalid file content')
files.append(
(
'Files',
(
file_.get('filename', ''),
io.BytesIO(content).read(),
file_.get('content_type', ''),
),
)
)
if not files:
return {}
data = {'Ids': post_data['worksrequests_ids']}
# return nothing if successful
self._call(
'api/WorksRequests/Attachments',
method='post',
files=files,
data=data,
return_response=True,
)
return {}
@endpoint(
methods=['get'],
name='worksrequest-status',
description=_('Get the status of a works request'),
parameters={
'worksrequests_id': {
'example_value': '1',
}
},
)
def worksrequest_status(self, request, worksrequests_id):
resp_data = self._call('api/WorksRequests/%s' % worksrequests_id)
resp_data['RequestStateLabel'] = STATUS_MAP.get(resp_data.get('RequestState', ''), '')
return {'data': resp_data}
@endpoint(
methods=['get'],
name='worksrequest-intervention-status',
description=_('Get the status of a works request intervention'),
parameters={
'number': {
'example_value': 'DIT23070011',
}
},
)
def worksrequest_intervention_status(self, request, number):
resp_data = self._call('/api/WorksRequests/GetInterventionStates', params={'number': number})
resp_data = resp_data[0] if resp_data else {}
resp_data['WorkStateLabel'] = INTERVENTION_STATUS_MAP.get(resp_data.get('WorkState', ''), '')
return {'data': resp_data}