contrib.maarch: add connector to maarch letterbox

This commit is contained in:
Thomas NOËL 2015-06-12 00:20:13 +02:00
parent 2fbcc9150f
commit a7c904f477
10 changed files with 589 additions and 0 deletions

View File

@ -0,0 +1,17 @@
Connect Publik with Maarch LetterBox
====================================
Compatible with Maarch 1.4 SOAP webservices.
http://maarchcourrier.com/
How to use
----------
1) Install python-suds
2) Add to your settings.py
# local_settings.py:
INSTALLED_APPS += ('passerelle.contrib.maarch',)
PASSERELLE_APPS += ('maarch',)

View File

@ -0,0 +1,27 @@
# passerelle.contrib.maarch
# Copyright (C) 2015 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 django.apps
class AppConfig(django.apps.AppConfig):
name = 'passerelle.contrib.maarch'
label = 'maarch'
def get_after_urls(self):
from . import urls
return urls.urlpatterns
default_app_config = 'passerelle.contrib.maarch.AppConfig'

View File

@ -0,0 +1,35 @@
# passerelle.contrib.maarch
# Copyright (C) 2015 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/>.
from django.utils.text import slugify
from django import forms
from .models import Management
class ManagementForm(forms.ModelForm):
class Meta:
model = Management
exclude = ('slug', 'users')
def save(self, commit=True):
if not self.instance.slug:
self.instance.slug = slugify(self.instance.title)
return super(ManagementForm, self).save(commit=commit)
class ManagementUpdateForm(ManagementForm):
class Meta:
model = Management
exclude = ('users',)

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('base', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Management',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('title', models.CharField(max_length=50)),
('slug', models.SlugField()),
('description', models.TextField()),
('wsdl_url', models.CharField(help_text='Maarch WSDL URL', max_length=128, verbose_name='WSDL URL')),
('verify_cert', models.BooleanField(default=True, verbose_name='Check HTTPS Certificate validity')),
('username', models.CharField(max_length=128, verbose_name='Username', blank=True)),
('password', models.CharField(max_length=128, verbose_name='Password', blank=True)),
('keystore', models.FileField(help_text='Certificate and private key in PEM format', upload_to=b'maarch', null=True, verbose_name='Keystore', blank=True)),
('users', models.ManyToManyField(to='base.ApiUser', blank=True)),
],
options={
'verbose_name': 'Maarch',
},
bases=(models.Model,),
),
]

View File

@ -0,0 +1,55 @@
# passerelle.contrib.maarch
# Copyright (C) 2015 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/>.
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.translation import ugettext_lazy as _
from passerelle.base.models import BaseResource
class Management(BaseResource):
wsdl_url = models.CharField(max_length=128, blank=False,
verbose_name=_('WSDL URL'),
help_text=_('Maarch WSDL URL'))
verify_cert = models.BooleanField(default=True,
verbose_name=_('Check HTTPS Certificate validity'))
username = models.CharField(max_length=128, blank=True,
verbose_name=_('Username'))
password = models.CharField(max_length=128, blank=True,
verbose_name=_('Password'))
keystore = models.FileField(upload_to='maarch', null=True, blank=True,
verbose_name=_('Keystore'),
help_text=_('Certificate and private key in PEM format'))
category = _('Business Process Connectors')
class Meta:
verbose_name = _('Maarch')
@classmethod
def get_icon_class(cls):
return 'ressources'
@classmethod
def get_verbose_name(cls):
return cls._meta.verbose_name
def get_absolute_url(self):
return reverse('maarch-view', kwargs={'slug': self.slug})
@classmethod
def get_add_url(cls):
return reverse('maarch-add')

View File

@ -0,0 +1,119 @@
# passerelle.contrib.maarch
# Copyright (C) 2015 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/>.
# borrowed from https://pypi.python.org/pypi/suds_requests
# and https://docs.oracle.com/cd/E50245_01/E50253/html/vmprg-soap-example-authentication-python.html
import requests
try:
import cStringIO as StringIO
except ImportError:
import StringIO
from suds.transport.http import HttpAuthenticated
from suds.transport import Reply
from suds.client import Client
from suds.sudsobject import asdict
from suds.plugin import MessagePlugin
# FIXME: WSDL incomplet, ne reference pas le schema suivant
# voir https://fedorahosted.org/suds/ticket/220 pour le fix
from suds.xsd.doctor import ImportDoctor, Import
doctor = ImportDoctor(Import('http://schemas.xmlsoap.org/soap/encoding/'))
class Transport(HttpAuthenticated):
def __init__(self, model, **kwargs):
self.model = model
HttpAuthenticated.__init__(self, **kwargs) # oldstyle class...
def get_requests_kwargs(self):
kwargs = {}
if self.model.username:
kwargs['auth'] = (self.model.username, self.model.password)
if self.model.keystore:
kwargs['cert'] = (self.model.keystore.path, self.model.keystore.path)
if not self.model.verify_cert:
kwargs['verify'] = False
return kwargs
def open(self, request):
resp = requests.get(request.url, headers=request.headers,
**self.get_requests_kwargs())
return StringIO.StringIO(resp.content)
def send(self, request):
self.addcredentials(request)
resp = requests.post(request.url, data=request.message,
headers=request.headers, **self.get_requests_kwargs())
result = Reply(resp.status_code, resp.headers, resp.content)
return result
class FixesPlugin(MessagePlugin):
def marshalled(self, context):
gedId = context.envelope.getChild('Body').getChild('viewResource').getChild('gedId')
if gedId is not None:
gedId.set('xsi:type', 'xsd:int')
calledByWS = context.envelope.getChild('Body').getChild('viewResource').getChild('calledByWS')
if calledByWS is not None:
calledByWS.set('xsi:type', 'xsd:boolean')
def get_client(model):
transport = Transport(model)
return Client(model.wsdl_url, transport=transport,
plugins=[FixesPlugin()],
cache=None,
doctor=doctor) # see FIXME above
def client_to_jsondict(client):
"""return description of the client, as dict (for json export)"""
res = {}
for i, sd in enumerate(client.sd):
d = {}
d['tns'] = sd.wsdl.tns[1]
d['prefixes'] = dict(p for p in sd.prefixes)
d['ports'] = {}
for p in sd.ports:
d['ports'][p[0].name] = {}
for m in p[1]:
d['ports'][p[0].name][m[0]] = dict(
(mp[0], sd.xlate(mp[1])) for mp in m[1])
d['types'] = {}
for t in sd.types:
ft = client.factory.create(sd.xlate(t[0]))
d['types'][sd.xlate(t[0])] = unicode(ft)
res[sd.service.name] = d
return res
def recursive_asdict(d):
"""Convert Suds object into serializable format."""
out = {}
for k, v in asdict(d).iteritems():
if hasattr(v, '__keylist__'):
out[k] = recursive_asdict(v)
elif isinstance(v, list):
out[k] = []
for item in v:
if hasattr(item, '__keylist__'):
out[k].append(recursive_asdict(item))
else:
out[k].append(item)
else:
out[k] = v
return out

View File

@ -0,0 +1,54 @@
{% extends "passerelle/manage.html" %}
{% load i18n passerelle %}
{% block more-user-links %}
{{ block.super }}
{% if object.id %}
<a href="{% url 'maarch-view' slug=object.slug %}">{{ object.title }}</a>
{% endif %}
{% endblock %}
{% block appbar %}
<h2>Maarch - {{ object.title }}</h2>
{% if perms.passerelle_maarch.change_passerelle_maarch %}
<a rel="popup" class="button" href="{% url 'maarch-edit' slug=object.slug %}">{% trans 'edit' %}</a>
{% endif %}
{% if perms.passerelle_maarch.delete_passerelle_maarch %}
<a rel="popup" class="button" href="{% url 'maarch-delete' slug=object.slug %}">{% trans 'delete' %}</a>
{% endif %}
{% endblock %}
{% block content %}
<div>
<h3>{% trans 'Endpoints' %}</h3>
<ul>
<li>{% trans 'Check WSDL availability:' %} <a href="{% url 'maarch-ping' slug=object.slug %}"
>{{ site_base_uri }}{% url 'maarch-ping' slug=object.slug %}</a>[?debug]</li>
<li>{% trans 'Store a resource:' %} POST <a href="{% url 'maarch-resource' slug=object.slug %}"
>{{ site_base_uri }}{% url 'maarch-resource' slug=object.slug %}</a> (payload: JSON w.c.s. formdata)</li>
<li>{% trans 'Get a resource:' %} <a href="{% url 'maarch-resource-id' table_name='res_letterbox' resource_id=100 slug=object.slug %}"
>{{ site_base_uri }}{% url 'maarch-resource-id' table_name='res_letterbox' resource_id=100 slug=object.slug %}</a></li>
<li>{% trans 'Get a resource (+adr):' %} <a href="{% url 'maarch-resource-id-adr' table_name='res_letterbox' adr_table_name='adr_abc' resource_id=100 slug=object.slug %}"
>{{ site_base_uri }}{% url 'maarch-resource-id-adr' table_name='res_letterbox' adr_table_name='adr_abc' resource_id=100 slug=object.slug %}</a></li>
<li>{% trans 'Get contacts:' %} <a href="{% url 'maarch-contact' slug=object.slug %}"
>{{ site_base_uri }}{% url 'maarch-contact' slug=object.slug %}</a>[?where=conditon]</li>
<li>{% trans 'Get a contact:' %} <a href="{% url 'maarch-contact-id' contact_id=100 slug=object.slug %}"
>{{ site_base_uri }}{% url 'maarch-contact-id' contact_id=100 slug=object.slug %}</a></li>
</ul>
</div>
{% if perms.base.view_accessright %}
<div>
<h3>{% trans "Security" %}</h3>
<p>
{% trans 'Access is limited to the following API users:' %}
</p>
{% access_rights_table resource=object permission='can_access' %}
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,56 @@
# passerelle.contrib.maarch
# Copyright (C) 2015 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/>.
from django.conf.urls import patterns, include, url
from passerelle.urls_utils import decorated_includes, required, app_enabled
from django.contrib.auth.decorators import login_required
from .views import *
public_urlpatterns = patterns('',
url(r'^(?P<slug>[\w,-]+)/$', ManagementDetailView.as_view(),
name='maarch-view'),
url(r'^(?P<slug>[\w,-]+)/ping/$', PingView.as_view(),
name='maarch-ping'),
url(r'^(?P<slug>[\w,-]+)/resource/$', ResourceView.as_view(),
name='maarch-resource'),
url(r'^(?P<slug>[\w,-]+)/resource/(?P<table_name>[\w_-]+)/(?P<resource_id>\d+)/$', ResourceView.as_view(),
name='maarch-resource-id'),
url(r'^(?P<slug>[\w,-]+)/resource/(?P<table_name>[\w_-]+)/(?P<adr_table_name>[\w_-]*)/(?P<resource_id>\d+)/$', ResourceView.as_view(),
name='maarch-resource-id-adr'),
url(r'^(?P<slug>[\w,-]+)/contact/$', ContactView.as_view(),
name='maarch-contact'),
url(r'^(?P<slug>[\w,-]+)/contact/(?P<contact_id>\d+)/$', ContactView.as_view(),
name='maarch-contact-id'),
)
management_urlpatterns = patterns('',
url(r'^add$', ManagementCreateView.as_view(),
name='maarch-add'),
url(r'^(?P<slug>[\w,-]+)/edit$', ManagementUpdateView.as_view(),
name='maarch-edit'),
url(r'^(?P<slug>[\w,-]+)/delete$', ManagementDeleteView.as_view(),
name='maarch-delete'),
)
urlpatterns = required(
app_enabled('maarch'),
patterns('',
url(r'^maarch/', include(public_urlpatterns)),
url(r'^manage/maarch/',
decorated_includes(login_required, include(management_urlpatterns))),
)
)

View File

@ -0,0 +1,193 @@
# passerelle.contrib.maarch
# Copyright (C) 2015 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 json
import urlparse
import requests
from suds.client import Client
from django.core.urlresolvers import reverse
from django.views.generic import DetailView as GenericDetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.core.cache import cache
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from passerelle import utils
from .soap import get_client, client_to_jsondict, recursive_asdict
from .models import Management
from .forms import ManagementForm, ManagementUpdateForm
class ManagementDetailView(GenericDetailView):
model = Management
template_name = 'passerelle/contrib/maarch/detail.html'
class ManagementCreateView(CreateView):
model = Management
form_class = ManagementForm
template_name = 'passerelle/manage/service_form.html'
class ManagementUpdateView(UpdateView):
model = Management
form_class = ManagementUpdateForm
template_name = 'passerelle/manage/service_form.html'
class ManagementDeleteView(DeleteView):
model = Management
template_name = 'passerelle/manage/service_confirm_delete.html'
def get_success_url(self):
return reverse('manage-home')
class DetailView(GenericDetailView):
model = Management
def get_client(self):
return get_client(self.get_object())
def get_data(self, request, *args, **kwargs):
raise NotImplementedError
@utils.protected_api('can_access')
def get(self, request, *args, **kwargs):
data = self.get_data(request, *args, **kwargs)
return utils.response_for_json(request, data)
class PingView(DetailView):
def get_data(self, request, *args, **kwargs):
client = self.get_client()
res = {'ping': 'pong'}
if 'debug' in request.GET:
res['client'] = client_to_jsondict(client)
return res
class ResourceView(DetailView):
@method_decorator(csrf_exempt)
def dispatch(self, *args, **kwargs):
return super(ResourceView, self).dispatch(*args, **kwargs)
def get_data(self, request, resource_id=None, table_name=None, adr_table_name=None, *args, **kwargs):
client = self.get_client()
if resource_id:
if not adr_table_name:
adr_table_name = 'adr_x'
results = client.service.viewResource(int(resource_id),
table_name, adr_table_name, True)
else:
if 'where' in request.GET:
searchParams = client.factory.create('searchParams')
searchParams.whereClause = request.GET.get('where')
else:
searchParams = ''
results = client.service.Demo_searchResources(searchParams)
return recursive_asdict(results)
@utils.protected_api('can_access')
def post(self, request, *args, **kwargs):
client = self.get_client()
formdata = json.loads(request.body)
url = formdata['url']
# get formdef schema
p = urlparse.urlsplit(url)
scheme, netloc, path, query, fragment = \
p.scheme, p.netloc, p.path, p.query, p.fragment
schema_path = path.rsplit('/', 2)[0] + '/schema'
schema_url = urlparse.urlunsplit((scheme, netloc, schema_path, query, fragment))
schema = requests.get(schema_url).json()
# build document
attachments = []
document = u'<html>'
document += u'<h1>%s</h1>' % schema['name']
for field in schema['fields']:
part = ''
if field['type'] == 'page':
part = u'<hr /><h2>%s</h2>' % field['label']
elif field['type'] == 'title':
part = u'<h3>%s</h3>' % field['label']
elif 'varname' in field:
part = u'<dl>'
part += u'<dt>%s</dt>' % field['label']
value = formdata['fields'].get(field['varname'], '')
if value and field['type'] == 'file':
attachments.append(value)
value = '%s' % value['filename']
if value is None:
value = '---'
part += u'<dd>%s</dd>' % value
part += u'</dl>'
document += part
document += '</html>'
encodedFile = document.encode('utf-8').encode('base64')
fileFormat = 'html'
# Maarch metadata
options = schema.get('options', {})
status = options.get('status') or 'ATT' # ATT->QualificationBasket
subject = client.factory.create('arrayOfDataContent')
subject.column = 'subject'
subject.value = formdata['display_name']
subject.type = 'string'
maarch_meta = [subject]
for meta in ('initiator', 'type_id', 'destination', 'priority', 'dest_user', 'typist'):
if meta in options:
soapmeta = client.factory.create('arrayOfDataContent')
soapmeta.column = meta
soapmeta.value = options[meta]
soapmeta.type = 'string'
maarch_meta.append(soapmeta)
metadata = client.factory.create('arrayOfData')
metadata.value = maarch_meta
# Maarch target: letterbox
collId = 'letterbox_coll'
table = 'res_letterbox'
# store resource
results = client.service.storeResource(encodedFile, metadata, collId,
table, fileFormat, status)
data = recursive_asdict(results)
resId = data['resId']
# if resId exists, store attachments
if resId:
for attachment in attachments:
fileFormat = attachment['content_type'].split('/')[1] # FIXME (how ?)
client.service.storeAttachmentResource(resId, collId, attachment['content'],
fileFormat, attachment['filename'])
return utils.response_for_json(request, data)
class ContactView(DetailView):
def get_data(self, request, contact_id=None, *args, **kwargs):
client = self.get_client()
if contact_id:
searchParams = {'whereClause': "contact_id = '%s'" % contact_id}
elif 'where' in request.GET:
searchParams = {'whereClause': request.GET.get('where')}
else:
searchParams = ''
results = client.service.listContacts(searchParams)
return recursive_asdict(results)