Projet

Général

Profil

0001-create-iws-connector-24567.patch

Emmanuel Cazenave, 25 juin 2018 18:25

Télécharger (21,3 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              | 227 ++++++++++++++++++
 .../templates/iws/iwsconnector_detail.html    |  32 +++
 passerelle/contrib/iws/xsd/ReponseWS.xsd      |  42 ++++
 setup.py                                      |   2 +
 tests/settings.py                             |   1 +
 tests/test_iws.py                             | 115 +++++++++
 11 files changed, 456 insertions(+), 2 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
28 28
    python-dateutil,
29 29
    python-pyproj,
30 30
    python-pil,
31
    python-zeep
31
    python-zeep,
32
    python-jsonschema
33
    
32 34
Recommends: python-soappy, python-phpserialize
33 35
Description: Uniform access to multiple data sources and services (Python module)
34 36

  
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_url', 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_url = 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
        client = self.soap_client()
99
        header = client.get_element('%sIsiWsAuthHeader' % NS)
100
        header_value = header(
101
            IsiLogin=self.username, IsiPassword=self.password, IsiDataBaseID=self.database)
102
        client.set_default_soapheaders([header_value])
103
        service = client.create_service('%sIsiHelpDeskServiceSoap' % NS, self.operation_endpoint)
104

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

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

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

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

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

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

  
208
        iws_data = {
209
            'NO_APPEL': data['token'],
210
            'I_APP_DEMANDEUR': '%s, %s' % (data['lastname'], data['firstname']),
211
            'I_AP_TEL_DEMANDEU': data['tel_number'] or '',
212
            'I_AP_ADRESSEMAIL': data['email'],
213
            'I_AP_DATRDVAGENDA': data['date'],
214
            'C_STAPPEL': 'E',
215
            'I_AP_SERVICE': 'TELESERVICE',
216
            'I_AP_SOURCE': 'TELESERVICE',
217
            'C_ORIGINE': 'TELESERVICE',
218
            'C_BLOCAGE': 2,
219
            'I_AP_EMAIL': 'OUI',
220
            'I_AP_CNIL': 1,
221
            'I_AP_SMS': 'NON',
222
            'DE_SYMPAPPEL': data['description'] or '',
223
            'C_QUALIFICATIF': 'INTERVENTION',
224
        }
225
        iws_res = self._soap_call(iws_data, 'IsiUpdateAndGetCall')
226
        self._check_status(iws_res)
227
        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
103 103
            'python-dateutil',
104 104
            'Pillow',
105 105
            'python-magic',
106
            'jsonschema',
106 107
            'zeep'
107 108
        ],
108 109
        cmdclass={
......
111 112
            'install_lib': install_lib,
112 113
            'sdist': eo_sdist,
113 114
        },
115
      package_data={'passerelle': ['*.xsd']}
114 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_url='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
-