start Litteralis connector (#68813)
This commit is contained in:
parent
13ebe8b5bd
commit
239be4b54f
|
@ -0,0 +1,70 @@
|
|||
# Generated by Django 2.2.26 on 2022-09-07 16:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('base', '0029_auto_20210202_1627'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Litteralis',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('title', models.CharField(max_length=50, verbose_name='Title')),
|
||||
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
(
|
||||
'basic_auth_username',
|
||||
models.CharField(
|
||||
blank=True, max_length=128, verbose_name='Basic authentication username'
|
||||
),
|
||||
),
|
||||
(
|
||||
'basic_auth_password',
|
||||
models.CharField(
|
||||
blank=True, max_length=128, verbose_name='Basic authentication password'
|
||||
),
|
||||
),
|
||||
(
|
||||
'client_certificate',
|
||||
models.FileField(
|
||||
blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
|
||||
),
|
||||
),
|
||||
(
|
||||
'trusted_certificate_authorities',
|
||||
models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'),
|
||||
),
|
||||
(
|
||||
'verify_cert',
|
||||
models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'),
|
||||
),
|
||||
(
|
||||
'http_proxy',
|
||||
models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'),
|
||||
),
|
||||
('base_url', models.URLField(verbose_name='API URL')),
|
||||
(
|
||||
'users',
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name='_litteralis_users_+',
|
||||
related_query_name='+',
|
||||
to='base.ApiUser',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Litteralis',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,184 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2022 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 json
|
||||
import urllib
|
||||
|
||||
import requests
|
||||
from django.db import models
|
||||
from django.utils import dateparse
|
||||
from django.utils.timezone import make_aware
|
||||
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
|
||||
|
||||
from . import schemas
|
||||
|
||||
|
||||
def parse_datetime(datetime_str):
|
||||
try:
|
||||
obj = dateparse.parse_datetime(datetime_str)
|
||||
except ValueError:
|
||||
raise APIError("Invalid datetime: %s" % datetime_str)
|
||||
if obj is None:
|
||||
raise APIError("Invalid datetime format: %s" % datetime_str)
|
||||
return obj
|
||||
|
||||
|
||||
class Litteralis(BaseResource, HTTPResource):
|
||||
base_url = models.URLField(_('API URL'))
|
||||
|
||||
category = _('Business Process Connectors')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Litteralis')
|
||||
|
||||
def _call(self, path, method='get', data=None, files=None):
|
||||
url = urllib.parse.urljoin(self.base_url, path)
|
||||
kwargs = {}
|
||||
|
||||
if method == 'post':
|
||||
if not data:
|
||||
data = {}
|
||||
kwargs['json'] = data
|
||||
if files:
|
||||
kwargs['files'] = files
|
||||
|
||||
try:
|
||||
resp = getattr(self.requests, method)(url, **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.JSONDecodeError):
|
||||
err_data = {'response_text': resp.text}
|
||||
raise APIError(str(main_exc), data=err_data)
|
||||
|
||||
if resp.headers.get('Content-Type').startswith('application/json'):
|
||||
try:
|
||||
return resp.json()
|
||||
except (json.JSONDecodeError, requests.exceptions.JSONDecodeError) as e:
|
||||
raise APIError(str(e))
|
||||
|
||||
return resp.text
|
||||
|
||||
@endpoint(
|
||||
name='demandes-recues',
|
||||
description=_('Create submission'),
|
||||
perm='can_access',
|
||||
post={
|
||||
'request_body': {
|
||||
'schema': {
|
||||
'application/json': schemas.DEMANDES_RECUES,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
def demandes_recues(self, request, post_data):
|
||||
if post_data['demandeur'].get('raisonSociale'):
|
||||
pass
|
||||
else:
|
||||
for field in ('nom', 'prenom'):
|
||||
if not post_data['demandeur'].get(field):
|
||||
raise APIError('Missing <%s> in demandeur' % field)
|
||||
|
||||
def change_date_fields(data):
|
||||
if data is None:
|
||||
return None
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
res = {}
|
||||
for k, v in data.items():
|
||||
if k.startswith('date'):
|
||||
res[k] = make_aware(parse_datetime(v)).isoformat()
|
||||
else:
|
||||
res[k] = change_date_fields(v)
|
||||
return res
|
||||
|
||||
return {'data': self._call('demandes-recues', method='post', data=change_date_fields(post_data))}
|
||||
|
||||
@endpoint(
|
||||
name='upload',
|
||||
description=_('Upload file'),
|
||||
perm='can_access',
|
||||
post={
|
||||
'request_body': {
|
||||
'schema': {
|
||||
'application/json': schemas.UPLOAD,
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
def upload(self, request, post_data):
|
||||
try:
|
||||
file_byte_content = base64.b64decode(post_data['file']['content'])
|
||||
except (TypeError, binascii.Error):
|
||||
raise APIError("Can't decode file")
|
||||
|
||||
files = {
|
||||
'file': (post_data['file']['filename'], file_byte_content, post_data['file']['content_type'])
|
||||
}
|
||||
return {
|
||||
'data': self._call(
|
||||
'demandes-recues/%s/upload' % post_data['id_demande'],
|
||||
method='post',
|
||||
files=files,
|
||||
)
|
||||
}
|
||||
|
||||
@endpoint(
|
||||
methods=['get'],
|
||||
name='demandes-recues-reponses',
|
||||
description=_('Get submission status'),
|
||||
perm='can_access',
|
||||
parameters={
|
||||
'id_demande': {
|
||||
'example_value': '1',
|
||||
}
|
||||
},
|
||||
)
|
||||
def demandes_recues_reponses(self, request, id_demande, **kwargs):
|
||||
return {
|
||||
'data': self._call(
|
||||
'demandes-recues/%s/reponses' % id_demande,
|
||||
)
|
||||
}
|
||||
|
||||
@endpoint(
|
||||
methods=['get'],
|
||||
name='demandes-recues-arrete',
|
||||
description=_('Get submission decree'),
|
||||
perm='can_access',
|
||||
parameters={
|
||||
'id_demande': {
|
||||
'example_value': '1',
|
||||
}
|
||||
},
|
||||
)
|
||||
def demandes_recues_arrete(self, request, id_demande):
|
||||
return {
|
||||
'data': self._call(
|
||||
'demandes-recues/%s/arrete' % id_demande,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
# passerelle - uniform access to multiple data sources and services
|
||||
# Copyright (C) 2022 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/>.
|
||||
|
||||
|
||||
DEMANDES_RECUES = {
|
||||
'$schema': 'http://json-schema.org/draft-04/schema#',
|
||||
'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'fournisseur': {
|
||||
'type': 'string',
|
||||
},
|
||||
'idDemande': {
|
||||
'type': 'string',
|
||||
},
|
||||
'dateEnvoi': {
|
||||
'type': 'string',
|
||||
},
|
||||
'typeModele': {'type': 'string', 'enum': ['DA', 'DPS', 'DPV']},
|
||||
'demandeur': {
|
||||
'title': 'Demandeur',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'nom': {
|
||||
'type': 'string',
|
||||
},
|
||||
'prenom': {
|
||||
'type': 'string',
|
||||
},
|
||||
'raisonSociale': {
|
||||
'type': 'string',
|
||||
},
|
||||
'telephone': {
|
||||
'type': 'string',
|
||||
},
|
||||
'indicatifTel': {
|
||||
'type': 'string',
|
||||
},
|
||||
'mail': {
|
||||
'type': 'string',
|
||||
},
|
||||
'adresse': {
|
||||
'title': 'Adresse',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'num': {
|
||||
'type': 'string',
|
||||
},
|
||||
'rue': {
|
||||
'type': 'string',
|
||||
},
|
||||
'cp': {
|
||||
'type': 'string',
|
||||
},
|
||||
'insee': {
|
||||
'type': 'string',
|
||||
},
|
||||
'commune': {
|
||||
'type': 'string',
|
||||
},
|
||||
'pays': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
'required': ['rue', 'insee'],
|
||||
},
|
||||
},
|
||||
'required': ['mail', 'adresse'],
|
||||
},
|
||||
'destinataire': {
|
||||
'title': 'Destinataire',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'idCollectivite': {
|
||||
'type': 'string',
|
||||
},
|
||||
'nomCollectivite': {
|
||||
'type': 'string',
|
||||
},
|
||||
'idAgenceSGLK': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
'required': ['idCollectivite', 'nomCollectivite'],
|
||||
},
|
||||
'localisation': {
|
||||
'title': 'Localisation',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'num': {
|
||||
'type': 'string',
|
||||
},
|
||||
'extension': {
|
||||
'type': 'string',
|
||||
},
|
||||
'rue': {
|
||||
'type': 'string',
|
||||
},
|
||||
'complement': {
|
||||
'type': 'string',
|
||||
},
|
||||
'cp': {
|
||||
'type': 'string',
|
||||
},
|
||||
'localite': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
'geom': {'title': 'Geolocalisation', 'type': 'object'},
|
||||
'additionalInformation': {
|
||||
'title': 'Additionnal informations',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'typeDemande': {
|
||||
'type': 'string',
|
||||
},
|
||||
'dateDebut': {
|
||||
'type': 'string',
|
||||
},
|
||||
'dateFin': {
|
||||
'type': 'string',
|
||||
},
|
||||
},
|
||||
'required': ['typeDemande', 'dateDebut', 'dateFin'],
|
||||
},
|
||||
},
|
||||
'required': [
|
||||
'fournisseur',
|
||||
'idDemande',
|
||||
'dateEnvoi',
|
||||
'typeModele',
|
||||
'demandeur',
|
||||
'geom',
|
||||
'additionalInformation',
|
||||
],
|
||||
'unflatten': True,
|
||||
}
|
||||
|
||||
UPLOAD = {
|
||||
'$schema': 'http://json-schema.org/draft-04/schema#',
|
||||
'type': 'object',
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'id_demande': {
|
||||
'title': 'Litteralis demand identifier',
|
||||
'type': 'string',
|
||||
},
|
||||
'file': {
|
||||
'title': 'File object',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'filename': {
|
||||
'type': 'string',
|
||||
'description': 'Filename',
|
||||
},
|
||||
'content': {
|
||||
'type': 'string',
|
||||
'description': 'Content',
|
||||
},
|
||||
'content_type': {
|
||||
'type': 'string',
|
||||
'description': 'Content type',
|
||||
},
|
||||
},
|
||||
'required': ['content'],
|
||||
},
|
||||
},
|
||||
'required': ['id_demande', 'file'],
|
||||
}
|
|
@ -151,6 +151,7 @@ INSTALLED_APPS = (
|
|||
'passerelle.apps.holidays',
|
||||
'passerelle.apps.jsondatastore',
|
||||
'passerelle.apps.ldap',
|
||||
'passerelle.apps.litteralis',
|
||||
'passerelle.apps.maelis',
|
||||
'passerelle.apps.mdel',
|
||||
'passerelle.apps.mdel_ddpacs',
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import base64
|
||||
import json
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from passerelle.apps.litteralis.models import Litteralis
|
||||
from passerelle.base.models import AccessRight, ApiUser
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def connector(db):
|
||||
api = ApiUser.objects.create(username='all', keytype='', key='')
|
||||
connector = Litteralis.objects.create(
|
||||
base_url='http://litteralis.invalid/',
|
||||
basic_auth_username='foo',
|
||||
basic_auth_password='bar',
|
||||
slug='slug-litteralis',
|
||||
)
|
||||
obj_type = ContentType.objects.get_for_model(connector)
|
||||
AccessRight.objects.create(
|
||||
codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=connector.pk
|
||||
)
|
||||
return connector
|
||||
|
||||
|
||||
def test_demandes_recues(app, connector):
|
||||
params = {
|
||||
'fournisseur': 'the-fournisseur',
|
||||
'idDemande': '12-34',
|
||||
'dateEnvoi': '2022-10-03T12:43:18',
|
||||
'typeModele': 'DA',
|
||||
'demandeur': {
|
||||
'raisonSociale': 'the-world-company',
|
||||
'mail': 'contact@the-world-company.com',
|
||||
'adresse': {
|
||||
'rue': 'rue de la république',
|
||||
'insee': '69034',
|
||||
},
|
||||
},
|
||||
'destinataire': {
|
||||
'idCollectivite': '1',
|
||||
'nomCollectivite': 'Malakoff',
|
||||
},
|
||||
'geom': {'type': 'Point', 'coordinates': [48.866667, 2.333333]},
|
||||
'additionalInformation': {
|
||||
'typeDemande': 'Stationnement pour travaux',
|
||||
"dateDebut": "2019-12-04T14:33:13",
|
||||
"dateFin": "2019-12-09T14:33:13",
|
||||
},
|
||||
}
|
||||
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.post('http://litteralis.invalid/demandes-recues', status=200, json={'identifier': '1234'})
|
||||
resp = app.post_json('/litteralis/slug-litteralis/demandes-recues', params=params)
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 0
|
||||
assert json_resp['data'] == {'identifier': '1234'}
|
||||
assert len(rsps.calls) == 1
|
||||
req = rsps.calls[0].request
|
||||
assert req.headers['Content-Type'] == 'application/json'
|
||||
json_req = json.loads(req.body)
|
||||
assert json_req['dateEnvoi'] == '2022-10-03T12:43:18+00:00'
|
||||
|
||||
|
||||
def test_upload(app, connector):
|
||||
params = {
|
||||
'file': {
|
||||
'filename': 'bla',
|
||||
'content': base64.b64encode(b'who what').decode(),
|
||||
'content_type': 'text/plain',
|
||||
},
|
||||
'id_demande': '1234',
|
||||
}
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.post('http://litteralis.invalid/demandes-recues/1234/upload', status=200, body='')
|
||||
resp = app.post_json('/litteralis/slug-litteralis/upload', params=params)
|
||||
assert len(rsps.calls) == 1
|
||||
assert rsps.calls[0].request.headers['Content-Type'].startswith('multipart/form-data')
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 0
|
||||
assert json_resp['data'] == ''
|
||||
|
||||
|
||||
def test_demandes_recues_reponses(app, connector):
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.get('http://litteralis.invalid/demandes-recues/1234/reponses', status=200, json={'foo': 'bar'})
|
||||
resp = app.get('/litteralis/slug-litteralis/demandes-recues-reponses?id_demande=1234')
|
||||
assert len(rsps.calls) == 1
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 0
|
||||
assert json_resp['data'] == {'foo': 'bar'}
|
||||
|
||||
|
||||
def test_demandes_recues_arrete(app, connector):
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.get('http://litteralis.invalid/demandes-recues/1234/arrete', status=200, json={'foo': 'bar'})
|
||||
resp = app.get('/litteralis/slug-litteralis/demandes-recues-arrete?id_demande=1234')
|
||||
assert len(rsps.calls) == 1
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 0
|
||||
assert json_resp['data'] == {'foo': 'bar'}
|
||||
|
||||
|
||||
def test_demandes_recues_arrete_error_with_json(app, connector):
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.get('http://litteralis.invalid/demandes-recues/1234/arrete', status=403, json={'foo': 'bar'})
|
||||
resp = app.get('/litteralis/slug-litteralis/demandes-recues-arrete?id_demande=1234')
|
||||
assert len(rsps.calls) == 1
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 1
|
||||
assert json_resp['data'] == {'foo': 'bar'}
|
||||
|
||||
|
||||
def test_demandes_recues_arrete_error_with_no_json(app, connector):
|
||||
with responses.RequestsMock() as rsps:
|
||||
rsps.get(
|
||||
'http://litteralis.invalid/demandes-recues/1234/arrete', status=403, body='something went wrong'
|
||||
)
|
||||
resp = app.get('/litteralis/slug-litteralis/demandes-recues-arrete?id_demande=1234')
|
||||
assert len(rsps.calls) == 1
|
||||
json_resp = resp.json
|
||||
assert json_resp['err'] == 1
|
||||
assert json_resp['err_desc'].startswith('403 Client Error')
|
Loading…
Reference in New Issue