Projet

Général

Profil

0001-create-iws-connector-24567.patch

Emmanuel Cazenave, 25 juin 2018 12:19

Télécharger (21,5 ko)

Voir les différences:

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

 MANIFEST.in                                   |   2 +-
 debian/control                                |   4 +-
 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              | 228 ++++++++++++++++++
 .../templates/iws/iwsconnector_detail.html    |  32 +++
 passerelle/contrib/iws/xsd/ReponseWS.xsd      |  42 ++++
 setup.py                                      |   5 +-
 tests/settings.py                             |   1 +
 tests/test_iws.py                             | 115 +++++++++
 11 files changed, 459 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,
32
    python-zeep
31 33
Recommends: python-soappy, python-phpserialize
32 34
Description: Uniform access to multiple data sources and services (Python module)
33 35

  
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', '0006_resourcestatus'),
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 zeep import Client
27
from zeep.transports import Transport
28

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

  
33

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

  
36
NS = '{http://isilog.fr}'
37

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

  
80

  
81
class IWSConnector(BaseResource):
82
    wsdl_endpoint = models.URLField(
83
        max_length=400, verbose_name=_('SOAP wsdl endpoint'),
84
        help_text=_('URL of the SOAP wsdl endpoint'))
85
    operation_endpoint = models.URLField(
86
        max_length=400, verbose_name=_('SOAP operation endpoint'),
87
        help_text=_('URL of SOAP operation endpoint'))
88
    username = models.CharField(max_length=128, verbose_name=_('Service username'))
89
    password = models.CharField(
90
        max_length=128, verbose_name=_('Service password'), null=True, blank=True)
91
    database = models.CharField(max_length=128, verbose_name=_('Service database'))
92
    category = _('Business Process Connectors')
93

  
94
    class Meta:
95
        verbose_name = _('IWS connector')
96

  
97
    def _soap_call(self, iws_data, method):
98
        transport = Transport(session=self.requests)
99
        client = Client(self.wsdl_endpoint, transport=transport)
100
        header = client.get_element('%sIsiWsAuthHeader' % NS)
101
        header_value = header(
102
            IsiLogin=self.username, IsiPassword=self.password, IsiDataBaseID=self.database)
103
        client.set_default_soapheaders([header_value])
104
        service = client.create_service('%sIsiHelpDeskServiceSoap' % NS, self.operation_endpoint)
105

  
106
        self.logger.debug("calling %s method of iws", method, extra={'data': iws_data})
107
        IsiWsEntity = client.get_type('%sIsiWsEntity' % NS)
108
        ArrayOfIsiWsDataField = client.get_type('%sArrayOfIsiWsDataField' % NS)
109
        IsiWsDataField = client.get_type('%sIsiWsDataField' % NS)
110
        ll = []
111
        for field, value in iws_data.items():
112
            ll.append(IsiWsDataField(IsiField=field, IsiValue=value))
113

  
114
        soap_list = ArrayOfIsiWsDataField(ll)
115
        ws_entity = IsiWsEntity(IsiFields=soap_list)
116
        iws_res = getattr(service, method)(ws_entity)
117

  
118
        schema_root = lxml.etree.parse(pkg_resources.resource_stream(
119
            'passerelle.contrib.iws', 'xsd/ReponseWS.xsd'))
120
        schema = lxml.etree.XMLSchema(schema_root)
121
        parser = lxml.etree.XMLParser(schema=schema, encoding='utf-8')
122
        try:
123
            tree = lxml.etree.fromstring(iws_res.encode('utf-8'), parser).getroottree()
124
        except lxml.etree.XMLSyntaxError:
125
            raise APIError("IWS response is not valid")
126
        result = {
127
            "status": tree.find('//Statut').text,
128
            "trace": tree.find('//Trace').text
129
        }
130
        fields = {}
131
        for data_field in tree.xpath('//IsiWsDataField'):
132
            fields[data_field.find('IsiField').text] = data_field.find('IsiValue').text
133
        result['fields'] = fields
134
        self.logger.debug("recieved data from %s method of iws", method, extra={'data': result})
135
        return result
136

  
137
    def _check_status(self, iws_res):
138
        if iws_res['status'] != 'responseOk':
139
            raise APIError('iws error, status: "%(status)s", trace: "%(trace)s"' % iws_res)
140

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

  
198
    @endpoint(methods=['post'], perm='can_access')
199
    def bookdate(self, request):
200
        try:
201
            data = json.loads(request.body)
202
        except ValueError as e:
203
            raise APIError("could not decode body to json: %s" % e, http_status=400)
204
        try:
205
            validate(data, BOOKDATE_SCHEMA)
206
        except ValidationError as e:
207
            raise APIError(e.message, http_status=400)
208

  
209
        iws_data = {
210
            'NO_APPEL': data['token'],
211
            'I_APP_DEMANDEUR': '%s, %s' % (data['lastname'], data['firstname']),
212
            'I_AP_TEL_DEMANDEU': data['tel_number'] or '',
213
            'I_AP_ADRESSEMAIL': data['email'],
214
            'I_AP_DATRDVAGENDA': data['date'],
215
            'C_STAPPEL': 'E',
216
            'I_AP_SERVICE': 'TELESERVICE',
217
            'I_AP_SOURCE': 'TELESERVICE',
218
            'C_ORIGINE': 'TELESERVICE',
219
            'C_BLOCAGE': 2,
220
            'I_AP_EMAIL': 'OUI',
221
            'I_AP_CNIL': 1,
222
            'I_AP_SMS': 'NON',
223
            'DE_SYMPAPPEL': data['description'] or '',
224
            'C_QUALIFICATIF': 'INTERVENTION',
225
        }
226
        iws_res = self._soap_call(iws_data, 'IsiUpdateAndGetCall')
227
        self._check_status(iws_res)
228
        return {'data': iws_res['fields']}
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',
107
            'zeep'
106 108
        ],
107 109
        cmdclass={
108 110
            'build': build,
......
110 112
            'install_lib': install_lib,
111 113
            'sdist': eo_sdist,
112 114
        },
115
      package_data={'passerelle': ['*.xsd']}
113 116
)
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
    import passerelle.contrib.iws.models
35
    monkeypatch.setattr(passerelle.contrib.iws.models.IWSConnector, '_soap_call', mock_soap_call)
36
    return mock_soap_call
37

  
38

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

  
47

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

  
56

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

  
68

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

  
83

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

  
102

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