498 lines
16 KiB
Python
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}
|