Projet

Général

Profil

0001-create-iws-connector-24567.patch

Emmanuel Cazenave, 20 juin 2018 10:14

Télécharger (21,9 ko)

Voir les différences:

Subject: [PATCH] create iws connector (#24567)

 MANIFEST.in                                   |   2 +-
 debian/control                                |   3 +-
 passerelle/contrib/iws/__init__.py            |   0
 .../contrib/iws/migrations/0001_initial.py    |  33 +++
 passerelle/contrib/iws/migrations/__init__.py |   0
 passerelle/contrib/iws/models.py              | 243 ++++++++++++++++++
 .../templates/iws/iwsconnector_detail.html    |  32 +++
 passerelle/contrib/iws/xsd/ReponseWS.xsd      |  42 +++
 setup.py                                      |   4 +-
 tests/settings.py                             |   1 +
 tests/test_iws.py                             | 117 +++++++++
 11 files changed, 474 insertions(+), 3 deletions(-)
 create mode 100644 passerelle/contrib/iws/__init__.py
 create mode 100644 passerelle/contrib/iws/migrations/0001_initial.py
 create mode 100644 passerelle/contrib/iws/migrations/__init__.py
 create mode 100644 passerelle/contrib/iws/models.py
 create mode 100644 passerelle/contrib/iws/templates/iws/iwsconnector_detail.html
 create mode 100644 passerelle/contrib/iws/xsd/ReponseWS.xsd
 create mode 100644 tests/test_iws.py
MANIFEST.in
1
recursive-include passerelle *.html *.mako *.txt README
1
recursive-include passerelle *.html *.mako *.txt *.xsd README
2 2
recursive-include passerelle/static *
3 3
recursive-include passerelle/apps/*/static *
4 4

  
debian/control
27 27
    python-lxml,
28 28
    python-dateutil,
29 29
    python-pyproj,
30
    python-pil
30
    python-pil,
31
    python-jsonschema,
31 32
Recommends: python-soappy, python-phpserialize
32 33
Description: Uniform access to multiple data sources and services (Python module)
33 34

  
passerelle/contrib/iws/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('base', '0007_auto_20180619_1456'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='IWSConnector',
16
            fields=[
17
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
                ('title', models.CharField(max_length=50, verbose_name='Title')),
19
                ('description', models.TextField(verbose_name='Description')),
20
                ('slug', models.SlugField(unique=True)),
21
                ('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL'), (b'FATAL', b'FATAL')])),
22
                ('wsdl_endpoint', models.URLField(help_text='URL of the SOAP wsdl endpoint', max_length=400, verbose_name='SOAP wsdl endpoint')),
23
                ('operation_endpoint', models.URLField(help_text='URL of SOAP operation endpoint', max_length=400, verbose_name='SOAP operation endpoint')),
24
                ('username', models.CharField(max_length=128, verbose_name='Service username')),
25
                ('password', models.CharField(max_length=128, null=True, verbose_name='Service password', blank=True)),
26
                ('database', models.CharField(max_length=128, verbose_name='Service database')),
27
                ('users', models.ManyToManyField(to='base.ApiUser', blank=True)),
28
            ],
29
            options={
30
                'verbose_name': 'IWS connector',
31
            },
32
        ),
33
    ]
passerelle/contrib/iws/models.py
1
# passerelle.contrib.iws
2
# Copyright (C) 2016  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from datetime import datetime
18
import json
19

  
20
from django.db import models
21
from django.utils import dateformat
22
from django.utils.translation import ugettext_lazy as _
23
from jsonschema import validate, ValidationError
24
import lxml.etree
25
import pkg_resources
26
from suds.client import Client
27

  
28
from passerelle.base.models import BaseResource
29
from passerelle.utils.api import endpoint
30
from passerelle.utils.jsonresponse import APIError
31

  
32

  
33
CODE_EQUIPE = {"DECHET": "DMT", "ENCOMBRANT": "VPVIGIE"}
34

  
35

  
36
BOOKDATE_SCHEMA = {
37
    "$schema": "http://json-schema.org/draft-03/schema#",
38
    "title": "IWS",
39
    "description": "",
40
    "type": "object",
41
    "properties": {
42
        "firstname": {
43
            "description": "Firstname",
44
            "type": "string",
45
            "required": True
46
        },
47
        "lastname": {
48
            "description": "Lastname",
49
            "type": "string",
50
            "required": True
51
        },
52
        "email": {
53
            "description": "Email",
54
            "type": "string",
55
            "required": True
56
        },
57
        "description": {
58
            "description": "Description of the request",
59
            "type": "string",
60
        },
61
        "tel_number": {
62
            "description": "Telephone number",
63
            "type": "string",
64
        },
65
        "date": {
66
            "description": "Booking date",
67
            "type": "string",
68
            "required": True
69
        },
70
        "token": {
71
            "description": "Booking token",
72
            "type": "string",
73
            "required": True
74
        }
75
    }
76
}
77

  
78

  
79
def create_client(wsdl_endpoint, operation_endpoint, username, password, database):
80
    client = Client(wsdl_endpoint, location=operation_endpoint)
81
    auth_header = client.factory.create('IsiWsAuthHeader')
82
    auth_header.IsiLogin = username
83
    auth_header.IsiPassword = password
84
    auth_header.IsiDataBaseID = database
85
    client.set_options(soapheaders=auth_header)
86
    return client
87

  
88

  
89
def soap_call(logger, client, iws_data, method):
90
    logger.debug("sending to iws : %s", iws_data)
91
    IsiWsEntity = client.factory.create('IsiWsEntity')
92
    ArrayOfIsiWsDataField = client.factory.create('ArrayOfIsiWsDataField')
93
    for field, value in iws_data.items():
94
        IsiWsDataField = client.factory.create('IsiWsDataField')
95
        IsiWsDataField.IsiField = field
96
        IsiWsDataField.IsiValue = value
97
        ArrayOfIsiWsDataField.IsiWsDataField.append(IsiWsDataField)
98
    IsiWsEntity.IsiFields = ArrayOfIsiWsDataField
99
    iws_res = getattr(client.service, method)(IsiWsEntity)
100

  
101
    schema_root = lxml.etree.parse(pkg_resources.resource_stream(
102
        'passerelle.contrib.iws', 'xsd/ReponseWS.xsd'))
103
    schema = lxml.etree.XMLSchema(schema_root)
104
    parser = lxml.etree.XMLParser(schema=schema, encoding='utf-8')
105
    try:
106
        tree = lxml.etree.fromstring(iws_res.encode('utf-8'), parser).getroottree()
107
    except lxml.etree.XMLSyntaxError:
108
        raise APIError("IWS response is not valid")
109
    result = {
110
        "status": tree.find('//Statut').text,
111
        "trace": tree.find('//Trace').text
112
    }
113
    fields = {}
114
    for data_field in tree.xpath('//IsiWsDataField'):
115
        fields[data_field.find('IsiField').text] = data_field.find('IsiValue').text
116
    result['fields'] = fields
117
    logger.debug("recieved from iws : %s", result)
118
    return result
119

  
120

  
121
class IWSConnector(BaseResource):
122
    wsdl_endpoint = models.URLField(
123
        max_length=400, verbose_name=_('SOAP wsdl endpoint'),
124
        help_text=_('URL of the SOAP wsdl endpoint'))
125
    operation_endpoint = models.URLField(
126
        max_length=400, verbose_name=_('SOAP operation endpoint'),
127
        help_text=_('URL of SOAP operation endpoint'))
128
    username = models.CharField(max_length=128, verbose_name=_('Service username'))
129
    password = models.CharField(
130
        max_length=128, verbose_name=_('Service password'), null=True, blank=True)
131
    database = models.CharField(max_length=128, verbose_name=_('Service database'))
132
    category = _('Business Process Connectors')
133

  
134
    class Meta:
135
        verbose_name = _('IWS connector')
136

  
137
    def _create_client(self):
138
        return create_client(
139
            self.wsdl_endpoint, self.operation_endpoint, self.username, self.password,
140
            self.database)
141

  
142
    @endpoint(
143
        methods=['get'], perm='can_access', example_pattern='{sti_code}/{request_type}/{volume}/',
144
        pattern='^(?P<sti_code>[0-9]{16})/(?P<request_type>\w+)/(?P<volume>[0-9]+)/$',
145
        parameters={
146
            'sti_code': {
147
                'description': _('Adrress STI code'), 'example_value': '3155570464130003'
148
            },
149
            'request_type': {
150
                'description': _('DECHET or ENCOMBRANT'),
151
                'example_value': 'DECHET'
152
            },
153
            'volume': {
154
                'description': _('Volume of waste'),
155
                'example_value': '1'
156
            },
157
            'city': {
158
                'description': _('City'),
159
                'example_value': 'TOULOUSE'
160
            },
161
        }
162
    )
163
    def checkdate(self, request, sti_code, request_type, volume, city):
164
        if request_type not in ('DECHET', 'ENCOMBRANT'):
165
            raise APIError("request_type should be 'DECHET' or 'ENCOMBRANT'")
166
        iws_data = {
167
            'C_ORIGINE': 'TELESERVICE',
168
            'I_APP_TYPEDEM': request_type,
169
            'I_AP_QTEAGENDA': '1' if request_type == 'DECHET' else volume,
170
            'C_STAPPEL': 'E',
171
            'C_NATURE': 'INC',
172
            'DE_SYMPAPPEL': 'booking description',
173
            'I_AP_COMMUNE': city,
174
            'I_AP_COMMUNEINTER': sti_code,
175
            'J_PRJACTPREV': '5',
176
            'C_EQUIPE': CODE_EQUIPE[request_type],
177
            'I_APP_DEMANDEUR': 'booking, demandeur',
178
            'I_AP_ADRESSEMAIL': 'booking@localhost'
179
        }
180
        iws_res = soap_call(self.logger, self._create_client(), iws_data, 'IsiAddAndGetCall')
181
        self._check_status(iws_res)
182
        iws_fields = iws_res['fields']
183
        token = iws_fields['NO_APPEL']
184
        if not token:
185
            raise APIError('iws error, missing token')
186
        dates = []
187
        result = {'data': dates}
188
        iws_dates = iws_fields['I_APP_DATESPOSSIBLES']
189
        if iws_dates == 'Aucune dates disponibles':
190
            return result
191
        for raw_date in iws_dates.split(';'):
192
            if raw_date:
193
                raw_date = raw_date.strip()
194
                date_obj = datetime.strptime(raw_date, '%d/%m/%Y').date()
195
                date_text = dateformat.format(date_obj, 'l d F Y')
196
                dates.append({"id": raw_date, "text": date_text, "token": token})
197
        return result
198

  
199
    @endpoint(methods=['post'], perm='can_access')
200
    def bookdate(self, request):
201
        error, error_msg, data = self._validate_inputs(request.body, BOOKDATE_SCHEMA)
202
        if error:
203
            self.logger.debug("received invalid data: %s", error_msg)
204
            raise APIError(error_msg, http_status=400)
205

  
206
        iws_data = {
207
            'NO_APPEL': data['token'],
208
            'I_APP_DEMANDEUR': '%s, %s' % (data['lastname'], data['firstname']),
209
            'I_AP_TEL_DEMANDEU': data['tel_number'] or '',
210
            'I_AP_ADRESSEMAIL': data['email'],
211
            'I_AP_DATRDVAGENDA': data['date'],
212
            'C_STAPPEL': 'E',
213
            'I_AP_SERVICE': 'TELESERVICE',
214
            'I_AP_SOURCE': 'TELESERVICE',
215
            'C_ORIGINE': 'TELESERVICE',
216
            'C_BLOCAGE': 2,
217
            'I_AP_EMAIL': 'OUI',
218
            'I_AP_CNIL': 1,
219
            'I_AP_SMS': 'NON',
220
            'DE_SYMPAPPEL': data['description'] or '',
221
            'C_QUALIFICATIF': 'INTERVENTION',
222
        }
223
        iws_res = soap_call(self.logger, self._create_client(), iws_data, 'IsiUpdateAndGetCall')
224
        self._check_status(iws_res)
225
        return {'data': iws_res['fields']}
226

  
227
    def _check_status(self, iws_res):
228
        if iws_res['status'] != 'responseOk':
229
            raise APIError('iws error, status: "%(status)s", trace: "%(trace)s"' % iws_res)
230

  
231
    def _validate_inputs(self, body, schema):
232
        """ process JSON body
233
        return a tuple (error, error_msg, data)
234
        """
235
        try:
236
            data = json.loads(body)
237
        except ValueError as e:
238
            return True, "could not decode body to json: %s" % e, None
239
        try:
240
            validate(data, schema)
241
        except ValidationError as e:
242
            return True, e.message, None
243
        return False, '', data
passerelle/contrib/iws/templates/iws/iwsconnector_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
3

  
4
{% block extra-sections %}
5
<div class="section">
6
  <ul>
7
    <li>
8
      <h4>{% trans 'Book date' %}</h4>
9
      {% url "generic-endpoint" connector="iws" slug=object.slug endpoint="bookdate" as bookdate %}
10
      <p> <strong>POST</strong> <a href="{{bookdate}}">{{bookdate}}</a></p>
11
      <pre>
12
        data_send = {
13
          'firstname': 'jon'
14
          'lastname': 'doe'
15
          'email': 'jon.doe@jondoe.com'
16
          'description': 'a refrigerator',
17
          'tel_number': '0102030405',
18
          'date': '26/10/2018',
19
          'token': 'XBNDNFT34'
20
        }
21
      </pre>
22
    </li>
23
  </ul>
24
</div>
25
{% endblock %}
26

  
27
{% block security %}
28
<p>
29
{% trans 'Book date is limited to the following API users:' %}
30
</p>
31
{% access_rights_table resource=object permission='can_access' %}
32
{% endblock %}
passerelle/contrib/iws/xsd/ReponseWS.xsd
1
<?xml version="1.0" encoding="utf-8" ?>
2
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
3
	<xsd:element name="IsiWsResponse">
4
		<xsd:complexType>
5
			<xsd:sequence>
6
				<xsd:element name="Statut" type="xsd:string"></xsd:element>
7
				<xsd:element name="Trace" type="xsd:string"></xsd:element>
8
				<xsd:element name="Objects" minOccurs="0">
9
					<xsd:complexType>
10
						<xsd:sequence>
11
							<xsd:element name="anyType" type="IsiWsEntity">
12
							</xsd:element>
13
						</xsd:sequence>
14
					</xsd:complexType>
15
				</xsd:element>
16
			</xsd:sequence>
17

  
18
		</xsd:complexType>
19
	</xsd:element>
20

  
21
	<xsd:complexType name="IsiWsEntity">
22
		<xsd:sequence>
23
			<xsd:element minOccurs="0" maxOccurs="1" name="IsiFields"
24
				type="ArrayOfIsiWsDataField" />
25
		</xsd:sequence>
26
	</xsd:complexType>
27
	<xsd:complexType name="ArrayOfIsiWsDataField">
28
		<xsd:sequence>
29
			<xsd:element minOccurs="0" maxOccurs="unbounded" name="IsiWsDataField"
30
				nillable="true" type="IsiWsDataField" />
31
		</xsd:sequence>
32
	</xsd:complexType>
33
	<xsd:complexType name="IsiWsDataField">
34
		<xsd:sequence>
35
			<xsd:element minOccurs="0" maxOccurs="1" name="IsiField"
36
				type="xsd:string" />
37
			<xsd:element minOccurs="0" maxOccurs="1" name="IsiValue"
38
				type="xsd:string" />
39
		</xsd:sequence>
40
	</xsd:complexType>
41

  
42
</xsd:schema>
setup.py
102 102
            'lxml',
103 103
            'python-dateutil',
104 104
            'Pillow',
105
            'python-magic'
105
            'python-magic',
106
            'jsonschema'
106 107
        ],
107 108
        cmdclass={
108 109
            'build': build,
......
110 111
            'install_lib': install_lib,
111 112
            'sdist': eo_sdist,
112 113
        },
114
      package_data={'passerelle': ['*.xsd']}
113 115
)
tests/settings.py
22 22
        'passerelle.contrib.greco',
23 23
        'passerelle.contrib.grenoble_gru',
24 24
        'passerelle.contrib.iparapheur',
25
        'passerelle.contrib.iws',
25 26
        'passerelle.contrib.maarch',
26 27
        'passerelle.contrib.mdel',
27 28
        'passerelle.contrib.meyzieu_newsletters',
tests/test_iws.py
1
from django.contrib.contenttypes.models import ContentType
2
from mock import Mock
3
import pytest
4

  
5
from passerelle.base.models import ApiUser, AccessRight
6
from passerelle.contrib.iws.models import IWSConnector
7

  
8

  
9
@pytest.fixture()
10
def setup(db):
11
    api = ApiUser.objects.create(username='all', keytype='', key='')
12
    conn = IWSConnector.objects.create(
13
        wsdl_endpoint='http://example.com/iws?wsdl',
14
        operation_endpoint='http://example.com/iws', username='admin', password='admin',
15
        database='somedb', slug='slug-iws')
16
    obj_type = ContentType.objects.get_for_model(conn)
17
    AccessRight.objects.create(
18
        codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=conn.pk)
19
    return conn
20

  
21

  
22
def create_params(**kwargs):
23
    res = {
24
        "firstname": "John", "lastname": "Doe", "email": "john.doe@localhost",
25
        "description": "four : 1", "tel_number": "0101010101", "date": "28/10/2018",
26
        "token": "token"
27
    }
28
    res.update(kwargs)
29
    return res
30

  
31

  
32
def mock_soap_call(monkeypatch, return_value):
33
    mock_soap_call = Mock(return_value=return_value)
34
    mock_soap_client = Mock()
35
    import passerelle.contrib.iws.models
36
    monkeypatch.setattr(passerelle.contrib.iws.models, 'soap_call', mock_soap_call)
37
    monkeypatch.setattr(passerelle.contrib.iws.models, 'create_client', mock_soap_client)
38
    return mock_soap_call, mock_soap_client
39

  
40

  
41
def test_checkdate_dechet_or_encombrant(app, setup):
42
    response = app.get(
43
        '/iws/slug-iws/checkdate/3155570464130003/error/3/?city=toulouse', expect_errors=True)
44
    json_result = response.json_body
45
    assert json_result['err'] == 1
46
    assert u'DECHET' in json_result['err_desc']
47
    assert u'ENCOMBRANT' in json_result['err_desc']
48

  
49

  
50
def test_checkdate_iws_error_status(app, setup, monkeypatch):
51
    mock_soap_call(monkeypatch, {'status': 'KO', 'trace': 'some trace'})
52
    response = app.get(
53
        '/iws/slug-iws/checkdate/3155570464130003/DECHET/3/?city=toulouse', expect_errors=True)
54
    json_result = response.json_body
55
    assert json_result['err'] == 1
56
    assert json_result['err_desc'] == 'iws error, status: "KO", trace: "some trace"'
57

  
58

  
59
def test_checkdate_iws_error_no_appel(app, setup, monkeypatch):
60
    mock_soap_call(
61
        monkeypatch, {
62
            'status': 'responseOk', 'trace': '',
63
            'fields': {'NO_APPEL': ''}})
64
    response = app.get(
65
        '/iws/slug-iws/checkdate/3155570464130003/DECHET/3/?city=toulouse', expect_errors=True)
66
    json_result = response.json_body
67
    assert json_result['err'] == 1
68
    assert json_result['err_desc'] == 'iws error, missing token'
69

  
70

  
71
def test_checkdate_iws_no_dates(app, setup, monkeypatch):
72
    mock_soap_call(
73
        monkeypatch, {
74
            'status': 'responseOk', 'trace': '',
75
            'fields': {
76
                'NO_APPEL': 'sometoken',
77
                'I_APP_DATESPOSSIBLES': 'Aucune dates disponibles'
78
            }
79
        })
80
    response = app.get('/iws/slug-iws/checkdate/3155570464130003/DECHET/3/?city=toulouse')
81
    json_result = response.json_body
82
    assert json_result['err'] == 0
83
    assert json_result['data'] == []
84

  
85

  
86
def test_checkdate_iws_has_dates(app, setup, monkeypatch, settings):
87
    settings.LANGUAGE_CODE = 'fr-fr'
88
    mock_soap_call(
89
        monkeypatch, {
90
            'status': 'responseOk', 'trace': '',
91
            'fields': {
92
                'NO_APPEL': 'sometoken',
93
                'I_APP_DATESPOSSIBLES': '18/06/2018; 19/06/2018'
94
            }
95
        })
96
    response = app.get('/iws/slug-iws/checkdate/3155570464130003/DECHET/3/?city=toulouse')
97
    json_result = response.json_body
98
    assert json_result['err'] == 0
99
    dates = json_result['data']
100
    assert len(dates) == 2
101
    assert dates[0] == {"id": "18/06/2018", "text": "lundi 18 juin 2018", "token": "sometoken"}
102
    assert dates[1] == {"id": "19/06/2018", "text": "mardi 19 juin 2018", "token": "sometoken"}
103

  
104

  
105
def test_bookdate(app, setup, monkeypatch):
106
    mock_soap_call(
107
        monkeypatch, {
108
            'status': 'responseOk', 'trace': '',
109
            'fields': {
110
                'NO_APPEL': 'sometoken',
111
                'I_APP_DATESPOSSIBLES': '18/06/2018;'
112
            }
113
        })
114
    response = app.post_json('/iws/slug-iws/bookdate/', params=create_params())
115
    json_result = response.json_body
116
    assert json_result['err'] == 0
117
    assert json_result['data'] == {'NO_APPEL': 'sometoken', 'I_APP_DATESPOSSIBLES': '18/06/2018;'}
0
-