pastell: add initial connector (#79105)
gitea/passerelle/pipeline/head This commit looks good
Details
gitea/passerelle/pipeline/head This commit looks good
Details
This commit is contained in:
parent
9626f03f34
commit
ccb53be16e
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 3.2.18 on 2023-06-28 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('base', '0030_resourcelog_base_resour_appname_298cbc_idx'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Pastell',
|
||||
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')),
|
||||
('api_base_url', models.URLField(max_length=128, verbose_name='API base URL')),
|
||||
('token', models.CharField(blank=True, max_length=128, verbose_name='API token')),
|
||||
('username', models.CharField(blank=True, max_length=128, verbose_name='API username')),
|
||||
('password', models.CharField(blank=True, max_length=128, verbose_name='API password')),
|
||||
(
|
||||
'users',
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name='_pastell_pastell_users_+',
|
||||
related_query_name='+',
|
||||
to='base.ApiUser',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Pastell',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,215 @@
|
|||
# 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
|
||||
from urllib import parse as urlparse
|
||||
|
||||
import requests
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from passerelle.base.models import BaseResource
|
||||
from passerelle.utils.api import endpoint
|
||||
from passerelle.utils.jsonresponse import APIError
|
||||
|
||||
DOCUMENT_CREATION_SCHEMA = {
|
||||
'$schema': 'http://json-schema.org/draft-04/schema#',
|
||||
'type': 'object',
|
||||
'required': ['type'],
|
||||
'additionalProperties': True,
|
||||
'properties': {'type': {'type': 'string'}},
|
||||
}
|
||||
|
||||
DOCUMENT_FILE_UPLOAD_SCHEMA = {
|
||||
'$schema': 'http://json-schema.org/draft-04/schema#',
|
||||
'type': 'object',
|
||||
'required': ['file', 'field_name'],
|
||||
'additionalProperties': False,
|
||||
'properties': {
|
||||
'field_name': {
|
||||
'type': 'string',
|
||||
'description': _('Document file\'s field name'),
|
||||
},
|
||||
'file': {
|
||||
'title': _('File object'),
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'description': _('Filename'),
|
||||
},
|
||||
'content': {
|
||||
'type': 'string',
|
||||
'description': _('Content'),
|
||||
},
|
||||
'content_type': {
|
||||
'type': 'string',
|
||||
'description': _('Content type'),
|
||||
},
|
||||
},
|
||||
'required': ['name', 'content'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Pastell(BaseResource):
|
||||
api_base_url = models.URLField(
|
||||
max_length=128,
|
||||
verbose_name=_('API base URL'),
|
||||
)
|
||||
token = models.CharField(max_length=128, blank=True, verbose_name=_('API token'))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('API username'))
|
||||
password = models.CharField(max_length=128, blank=True, verbose_name=_('API password'))
|
||||
|
||||
category = _('Business Process Connectors')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Pastell')
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
if not self.token and not self.username:
|
||||
raise ValidationError(_('API token or API username and API password should be defined.'))
|
||||
return super().clean(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def requests_options(self):
|
||||
options = {}
|
||||
if self.token:
|
||||
options['headers'] = {'Authorization': 'Bearer: %s' % self.token}
|
||||
elif self.username:
|
||||
options['auth'] = HTTPBasicAuth(self.username, self.password)
|
||||
return options
|
||||
|
||||
def call(self, path, method='get', params=None, **kwargs):
|
||||
url = urlparse.urljoin(self.api_base_url, path)
|
||||
try:
|
||||
response = self.requests.request(
|
||||
url=url, method=method, params=params, **kwargs, **self.requests_options
|
||||
)
|
||||
response.raise_for_status()
|
||||
except (requests.Timeout, requests.RequestException) as e:
|
||||
raise APIError(str(e))
|
||||
return response
|
||||
|
||||
def check_status(self):
|
||||
try:
|
||||
response = self.call('version')
|
||||
except APIError as e:
|
||||
raise Exception('Pastell server is down: %s' % e)
|
||||
return {'data': response.json()}
|
||||
|
||||
@endpoint(description=_('List entities'))
|
||||
def entities(self, request):
|
||||
data = []
|
||||
response = self.call('entite')
|
||||
for item in response.json():
|
||||
item['id'] = item['id_e']
|
||||
item['text'] = item['denomination']
|
||||
data.append(item)
|
||||
return {'data': data}
|
||||
|
||||
@endpoint(
|
||||
description=_('List entity documents'),
|
||||
parameters={'entity_id': {'description': _('Entity ID'), 'example_value': '42'}},
|
||||
)
|
||||
def documents(self, request, entity_id):
|
||||
data = []
|
||||
response = self.call('entite/%s/document' % entity_id)
|
||||
for item in response.json():
|
||||
item['id'] = item['id_d']
|
||||
item['text'] = item['titre']
|
||||
data.append(item)
|
||||
return {'data': data}
|
||||
|
||||
@endpoint(
|
||||
description=_('Get document details'),
|
||||
parameters={
|
||||
'entity_id': {'description': _('Entity ID'), 'example_value': '42'},
|
||||
'document_id': {'description': _('Document ID'), 'example_value': 'hDWtdSC'},
|
||||
},
|
||||
)
|
||||
def document(self, request, entity_id, document_id):
|
||||
response = self.call('entite/%s/document/%s' % (entity_id, document_id))
|
||||
return {'data': response.json()}
|
||||
|
||||
@endpoint(
|
||||
post={
|
||||
'description': _('Create a document for an entity'),
|
||||
'request_body': {'schema': {'application/json': DOCUMENT_CREATION_SCHEMA}},
|
||||
},
|
||||
parameters={
|
||||
'entity_id': {'description': _('Entity ID'), 'example_value': '42'},
|
||||
},
|
||||
)
|
||||
def create_document(self, request, entity_id, post_data):
|
||||
# create document
|
||||
response = self.call('entite/%s/document' % entity_id, 'post', params=post_data)
|
||||
document_id = response.json()['id_d']
|
||||
|
||||
# update it with other attributes
|
||||
response = self.call('entite/%s/document/%s' % (entity_id, document_id), 'patch', params=post_data)
|
||||
|
||||
return {'data': response.json()}
|
||||
|
||||
@endpoint(
|
||||
post={
|
||||
'description': _('Upload a file to a document'),
|
||||
'request_body': {'schema': {'application/json': DOCUMENT_FILE_UPLOAD_SCHEMA}},
|
||||
},
|
||||
parameters={
|
||||
'entity_id': {'description': _('Entity ID'), 'example_value': '42'},
|
||||
'document_id': {'description': _('Document ID'), 'example_value': 'hDWtdSC'},
|
||||
},
|
||||
)
|
||||
def upload_document_file(self, request, entity_id, document_id, post_data):
|
||||
filename = post_data['file']['name']
|
||||
file_data = {
|
||||
'file_content': (
|
||||
filename,
|
||||
base64.b64decode(post_data['file']['content']),
|
||||
post_data['file'].get('content_type'),
|
||||
)
|
||||
}
|
||||
|
||||
response = self.call(
|
||||
'entite/%s/document/%s/file/%s' % (entity_id, document_id, post_data['field_name']),
|
||||
'post',
|
||||
files=file_data,
|
||||
data={'file_name': filename},
|
||||
)
|
||||
return {'data': response.json()}
|
||||
|
||||
@endpoint(
|
||||
description=_('Get document\'s file'),
|
||||
parameters={
|
||||
'entity_id': {'description': _('Entity ID'), 'example_value': '42'},
|
||||
'document_id': {'description': _('Document ID'), 'example_value': 'hDWtdSC'},
|
||||
'field_name': {
|
||||
'description': _('Document file\'s field name'),
|
||||
'example_value': 'document',
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_document_file(self, request, entity_id, document_id, field_name):
|
||||
document = self.call('entite/%s/document/%s/file/%s' % (entity_id, document_id, field_name))
|
||||
response = HttpResponse(document.content, content_type=document.headers['Content-Type'])
|
||||
response['Content-Disposition'] = document.headers['Content-disposition']
|
||||
return response
|
|
@ -170,6 +170,7 @@ INSTALLED_APPS = (
|
|||
'passerelle.apps.orange',
|
||||
'passerelle.apps.ovh',
|
||||
'passerelle.apps.oxyd',
|
||||
'passerelle.apps.pastell',
|
||||
'passerelle.apps.pdf',
|
||||
'passerelle.apps.phonecalls',
|
||||
'passerelle.apps.photon',
|
||||
|
|
|
@ -0,0 +1,294 @@
|
|||
# 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 pytest
|
||||
import responses
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
|
||||
from passerelle.apps.pastell.models import Pastell
|
||||
from passerelle.base.models import AccessRight, ApiUser
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def setup(db):
|
||||
api = ApiUser.objects.create(username='all', keytype='', key='')
|
||||
conn = Pastell.objects.create(
|
||||
api_base_url='http://example.com/api/v2/', username='admin', password='admin', slug='test'
|
||||
)
|
||||
obj_type = ContentType.objects.get_for_model(conn)
|
||||
AccessRight.objects.create(
|
||||
codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=conn.pk
|
||||
)
|
||||
return conn
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_auth_headers(app, setup):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
'http://example.com/api/v2/version',
|
||||
json={"version": "3.0.4", "revision": 54322},
|
||||
status=200,
|
||||
)
|
||||
setup.check_status()
|
||||
assert len(responses.calls) == 1
|
||||
assert 'Basic ' in responses.calls[0].request.headers['Authorization']
|
||||
|
||||
setup.token = '12345'
|
||||
setup.save()
|
||||
setup.check_status()
|
||||
assert len(responses.calls) == 2
|
||||
assert responses.calls[1].request.headers['Authorization'] == 'Bearer: 12345'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_service_down_status(app, setup):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
'http://example.com/api/v2/version',
|
||||
status=404,
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
setup.check_status()
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_list_entities(app, setup):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
'http://example.com/api/v2/entite',
|
||||
json=[
|
||||
{
|
||||
"id_e": "7",
|
||||
"denomination": "Publik",
|
||||
"siren": "198307662",
|
||||
"type": "collectivite",
|
||||
"centre_de_gestion": "0",
|
||||
"entite_mere": "1",
|
||||
}
|
||||
],
|
||||
status=200,
|
||||
)
|
||||
url = reverse(
|
||||
'generic-endpoint', kwargs={'connector': 'pastell', 'endpoint': 'entities', 'slug': setup.slug}
|
||||
)
|
||||
resp = app.get(url)
|
||||
data = resp.json['data']
|
||||
assert data[0]['id'] == '7'
|
||||
assert data[0]['text'] == 'Publik'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_list_documents(app, setup):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
'http://example.com/api/v2/entite/7/document',
|
||||
json=[
|
||||
{
|
||||
"id_d": "MNYDNCa",
|
||||
"id_e": "7",
|
||||
"type": "publik",
|
||||
"titre": "TestConnecteur",
|
||||
"creation": "2023-06-27 18:02:04",
|
||||
"modification": "2023-06-27 18:02:28",
|
||||
"denomination": "Publik",
|
||||
"entite_base": "Publik",
|
||||
}
|
||||
],
|
||||
status=200,
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
'generic-endpoint', kwargs={'connector': 'pastell', 'endpoint': 'documents', 'slug': setup.slug}
|
||||
)
|
||||
resp = app.get(url, params={'entity_id': '7'})
|
||||
data = resp.json['data']
|
||||
assert data[0]['id'] == 'MNYDNCa'
|
||||
assert data[0]['text'] == 'TestConnecteur'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_get_document_details(app, setup):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
'http://example.com/api/v2/entite/7/document/MNYDNCa',
|
||||
json={
|
||||
"info": {
|
||||
"id_d": "MNYDNCa",
|
||||
"type": "publik",
|
||||
"titre": "TestConnecteur",
|
||||
"creation": "2023-06-27 18:02:04",
|
||||
"modification": "2023-06-27 18:02:28",
|
||||
},
|
||||
"data": {
|
||||
"envoi_signature": "checked",
|
||||
"envoi_iparapheur": "1",
|
||||
"nom_dossier": "TestConnecteur",
|
||||
"iparapheur_type": "Type Publik",
|
||||
"iparapheur_sous_type": "SPublik",
|
||||
},
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
'generic-endpoint', kwargs={'connector': 'pastell', 'endpoint': 'document', 'slug': setup.slug}
|
||||
)
|
||||
resp = app.get(url, params={'entity_id': '7', 'document_id': 'MNYDNCa'})
|
||||
data = resp.json['data']
|
||||
assert data['data']['nom_dossier'] == 'TestConnecteur'
|
||||
assert data['info']['id_d'] == 'MNYDNCa'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_create_document(app, setup):
|
||||
responses.add(
|
||||
responses.POST,
|
||||
'http://example.com/api/v2/entite/7/document',
|
||||
json={
|
||||
"info": {
|
||||
"id_d": "67WaYzM",
|
||||
"type": "publik",
|
||||
"titre": "",
|
||||
"creation": "2023-06-27 17:25:54",
|
||||
"modification": "2023-06-27 17:25:54",
|
||||
},
|
||||
"data": {"envoi_signature": "checked", "envoi_iparapheur": "1"},
|
||||
"action_possible": ["modification", "supression"],
|
||||
"action-possible": ["modification", "supression"],
|
||||
"last_action": {
|
||||
"action": "creation",
|
||||
"message": "Cr\u00e9ation du document",
|
||||
"date": "2023-06-27 17:25:54",
|
||||
},
|
||||
"id_d": "67WaYzM",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
responses.add(
|
||||
responses.PATCH,
|
||||
'http://example.com/api/v2/entite/7/document/67WaYzM',
|
||||
json={
|
||||
"content": {
|
||||
"info": {
|
||||
"id_d": "MNYDNCa",
|
||||
"type": "publik",
|
||||
"titre": "TestConnecteur",
|
||||
"creation": "2023-06-27 18:02:04",
|
||||
"modification": "2023-06-27 18:02:28",
|
||||
},
|
||||
"data": {
|
||||
"envoi_signature": "checked",
|
||||
"envoi_iparapheur": "1",
|
||||
"nom_dossier": "TestConnecteu",
|
||||
"iparapheur_type": "Type Publik",
|
||||
"iparapheur_sous_type": "SPublik",
|
||||
},
|
||||
},
|
||||
"result": "ok",
|
||||
"formulaire_ok": 0,
|
||||
"message": "Le formulaire est incomplet : le champ \u00abDocument\u00bb est obligatoire.",
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
'generic-endpoint', kwargs={'connector': 'pastell', 'endpoint': 'create_document', 'slug': setup.slug}
|
||||
)
|
||||
resp = app.post_json(
|
||||
'%s?entity_id=7' % url,
|
||||
params={
|
||||
'type': 'publik',
|
||||
'nom_dossier': 'TestConnecteur',
|
||||
'iparapheur_type': 'Type Publik',
|
||||
'iparapheur_sous_type': 'SPublik',
|
||||
},
|
||||
)
|
||||
assert len(responses.calls) == 2
|
||||
assert resp.json['data']['result'] == 'ok'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_add_document_file(app, setup):
|
||||
responses.add(
|
||||
responses.POST,
|
||||
'http://example.com/api/v2/entite/7/document/13fgg3E/file/document',
|
||||
json={
|
||||
'content': {
|
||||
'info': {
|
||||
'id_d': '13fgg3E',
|
||||
'type': 'publik',
|
||||
'titre': 'DocumentViaAPI',
|
||||
'creation': '2023-07-03 17:35:16',
|
||||
'modification': '2023-07-03 17:35:16',
|
||||
},
|
||||
'data': {
|
||||
'envoi_signature': 'checked',
|
||||
'envoi_iparapheur': '1',
|
||||
'nom_dossier': 'DocumentViaAPI',
|
||||
'iparapheur_type': 'Type Publik',
|
||||
'iparapheur_sous_type': 'SPublik',
|
||||
'document': ['TestSerghei.pdf'],
|
||||
},
|
||||
},
|
||||
'result': 'ok',
|
||||
'formulaire_ok': 1,
|
||||
'message': '',
|
||||
'err': 0,
|
||||
},
|
||||
status=201,
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
'generic-endpoint',
|
||||
kwargs={'connector': 'pastell', 'endpoint': 'upload_document_file', 'slug': setup.slug},
|
||||
)
|
||||
resp = app.post_json(
|
||||
'%s?entity_id=7&document_id=13fgg3E' % url,
|
||||
params={
|
||||
'field_name': 'document',
|
||||
'file': {
|
||||
'name': 'test.txt',
|
||||
'content': 'dGVzdA==',
|
||||
'content_type': 'text/plain',
|
||||
},
|
||||
},
|
||||
)
|
||||
assert resp.json['data']['result'] == 'ok'
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_get_document_file(app, setup):
|
||||
responses.add(
|
||||
responses.GET,
|
||||
'http://example.com/api/v2/entite/7/document/13fgg3E/file/document',
|
||||
body='test',
|
||||
content_type='text/plain',
|
||||
headers={'Content-disposition': 'attachment; filename=test.txt'},
|
||||
status=200,
|
||||
)
|
||||
|
||||
url = reverse(
|
||||
'generic-endpoint',
|
||||
kwargs={'connector': 'pastell', 'endpoint': 'get_document_file', 'slug': setup.slug},
|
||||
)
|
||||
resp = app.get(
|
||||
'%s?entity_id=7&document_id=13fgg3E&field_name=document' % url,
|
||||
)
|
||||
assert resp.headers.get('Content-Disposition') == 'attachment; filename=test.txt'
|
||||
assert resp.headers.get('Content-Type') == 'text/plain'
|
||||
assert resp.body == b'test'
|
Loading…
Reference in New Issue