From e0b6f1d68d21dc5c6a2ca6cff22eead62c1ccc13 Mon Sep 17 00:00:00 2001
From: Emmanuel Cazenave <ecazenave@entrouvert.com>
Date: Mon, 27 May 2019 19:00:44 +0200
Subject: [PATCH] start atal connector (#33348)

---
 passerelle/apps/atal/__init__.py              |   0
 .../apps/atal/migrations/0001_initial.py      |  31 ++++
 passerelle/apps/atal/migrations/__init__.py   |   0
 passerelle/apps/atal/models.py                | 169 ++++++++++++++++++
 passerelle/settings.py                        |   1 +
 tests/test_atal.py                            | 141 +++++++++++++++
 6 files changed, 342 insertions(+)
 create mode 100644 passerelle/apps/atal/__init__.py
 create mode 100644 passerelle/apps/atal/migrations/0001_initial.py
 create mode 100644 passerelle/apps/atal/migrations/__init__.py
 create mode 100644 passerelle/apps/atal/models.py
 create mode 100644 tests/test_atal.py

diff --git a/passerelle/apps/atal/__init__.py b/passerelle/apps/atal/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/apps/atal/migrations/0001_initial.py b/passerelle/apps/atal/migrations/0001_initial.py
new file mode 100644
index 00000000..3213a4cd
--- /dev/null
+++ b/passerelle/apps/atal/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2019-05-24 10:25
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('base', '0012_job'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ATALConnector',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=50, verbose_name='Title')),
+                ('description', models.TextField(verbose_name='Description')),
+                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
+                ('base_soap_url', models.URLField(help_text='URL of the base SOAP endpoint', max_length=400, verbose_name='Base SOAP endpoint')),
+                ('users', models.ManyToManyField(blank=True, related_name='_atalconnector_users_+', related_query_name='+', to='base.ApiUser')),
+            ],
+            options={
+                'verbose_name': 'ATAL connector',
+            },
+        ),
+    ]
diff --git a/passerelle/apps/atal/migrations/__init__.py b/passerelle/apps/atal/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/apps/atal/models.py b/passerelle/apps/atal/models.py
new file mode 100644
index 00000000..a1cf74d8
--- /dev/null
+++ b/passerelle/apps/atal/models.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+
+# passerelle.apps.atal
+# Copyright (C) 2019  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.db import models
+from django.utils.six.moves import urllib
+from django.utils.translation import ugettext_lazy as _
+import lxml.etree
+from zeep import helpers
+
+from passerelle.base.models import BaseResource
+from passerelle.utils.api import endpoint
+from passerelle.utils.jsonresponse import APIError
+
+
+INSERT_DEMANDE_BY_TYPE_SCHEMA = {
+    "$schema": "http://json-schema.org/draft-03/schema#",
+    "title": "",
+    "description": "",
+    "type": "object",
+    "properties": {
+        "contact_nom": {
+            "description": "Nom du contact",
+            "required": True
+        },
+        "contact_tel": {
+            "description": "Téléphone du contact",
+            "type": "string",
+        },
+        "contact_email": {
+            "description": "Email du contact",
+            "type": "string",
+        },
+        "contact_adresse": {
+            "description": "Adresse du contact",
+            "type": "string",
+        },
+        "demande_objet": {
+            "description": "Object de la demande",
+            "type": "string",
+        },
+        "demande_lieu": {
+            "description": "Lieu de la demande",
+            "type": "string",
+        },
+        "demande_description": {
+            "description": "Description de la demande",
+            "type": "string",
+        },
+        "remote_adresse": {
+            "description": "",
+            "type": "string"
+        },
+        "code_equipement": {
+            "description": "Code de l'équipement",
+            "type": "string"
+        },
+        "code_service_demandeur": {
+            "description": "Code du service demandeur",
+            "type": "string"
+        },
+        "date_souhaite": {
+            "description": "Date souhaitée",
+            "type": "string"
+        },
+        "type_demande": {
+            "description": "Type demande",
+            "type": "string"
+        }
+    }
+}
+
+
+class ATALConnector(BaseResource):
+    base_soap_url = models.URLField(
+        max_length=400, verbose_name=_('Base SOAP endpoint'),
+        help_text=_('URL of the base SOAP endpoint'))
+    category = _('Business Process Connectors')
+
+    class Meta:
+        verbose_name = _('ATAL connector')
+
+    def _soap_call(self, wsdl, method, **kwargs):
+        wsdl_url = urllib.parse.urljoin(self.base_soap_url, '%s?wsdl' % wsdl)
+        client = self.soap_client(wsdl_url=wsdl_url)
+        return getattr(client.service, method)(**kwargs)
+
+    def _basic_ref(self, wsdl, method):
+        soap_res = self._soap_call(wsdl=wsdl, method=method)
+        res = []
+        for elem in soap_res:
+            res.append({'id': elem.code, 'text': elem.libelle})
+        return {'data': res}
+
+    @endpoint(methods=['get'], perm='can_access')
+    def get_type_activite(self, request):
+        return self._basic_ref('VilleAgileService', 'getTypeActivite')
+
+    @endpoint(methods=['get'], perm='can_access')
+    def get_type_de_voie(self, request):
+        return self._basic_ref('VilleAgileService', 'getTypeDeVoie')
+
+    @endpoint(methods=['get'], perm='can_access')
+    def get_types_equipement(self, request):
+        soap_res = self._soap_call(wsdl='VilleAgileService', method='getTypesEquipement')
+        tree = lxml.etree.fromstring(soap_res.encode('utf-8')).getroottree()
+        types = tree.xpath('//types')[0]
+        res = []
+        for type_elem in types.getchildren():
+            res.append({'id': type_elem.get('id'), 'text': type_elem.get('label')})
+        return {'data': res}
+
+    @endpoint(
+        perm='can_access',
+        post={
+            'description': _('Insert Demande By Type'),
+            'request_body': {
+                'schema': {
+                    'application/json': INSERT_DEMANDE_BY_TYPE_SCHEMA
+                }
+            }
+        }
+    )
+    def insert_demande_by_type(self, request, post_data):
+        demande_number = self._soap_call(
+            wsdl='DemandeService', method='insertDemandeByType',
+            contactNom=post_data['contact_nom'],
+            contactTelephone=post_data['contact_telephone'],
+            contactCourriel=post_data['contact_email'],
+            contactAdresse=post_data['contact_adresse'], demandeObjet=post_data['demande_objet'],
+            demandeLieu=post_data['demande_lieu'],
+            demandeDescription=post_data['demande_description'],
+            remoteAddress=post_data['remote_adresse'], codeEquipement=post_data['code_equipement'],
+            codeServiceDemandeur=post_data['code_service_demandeur'],
+            dateSouhaitee=post_data['date_souhaite'], typeDemande=post_data['type_demande']
+        )
+        return {'data': {'demande_number': demande_number}}
+
+    @endpoint(
+        methods=['get'], perm='can_access', example_pattern='{demande_number}/',
+        pattern='^(?P<demande_number>\w+)/$',
+        parameters={
+            'demande_number': {
+                'description': _('Demande number'), 'example_value': 'DIT18050001'
+            }
+        }
+    )
+    def retrieve_details_demande(self, request, demande_number, **kwargs):
+        if not demande_number:
+            raise APIError('A demande_number parameter must be specified')
+        soap_res = self._soap_call(
+            wsdl='DemandeService', method='retrieveDetailsDemande',
+            demandeNumberParam=demande_number)
+        return {'data': helpers.serialize_object(soap_res)}
diff --git a/passerelle/settings.py b/passerelle/settings.py
index 6ce4274f..b6589902 100644
--- a/passerelle/settings.py
+++ b/passerelle/settings.py
@@ -123,6 +123,7 @@ INSTALLED_APPS = (
     'passerelle.apps.api_particulier',
     'passerelle.apps.arcgis',
     'passerelle.apps.arpege_ecp',
+    'passerelle.apps.atal',
     'passerelle.apps.atos_genesys',
     'passerelle.apps.base_adresse',
     'passerelle.apps.bdp',
diff --git a/tests/test_atal.py b/tests/test_atal.py
new file mode 100644
index 00000000..5b049351
--- /dev/null
+++ b/tests/test_atal.py
@@ -0,0 +1,141 @@
+from django.contrib.contenttypes.models import ContentType
+import mock
+import pytest
+
+from passerelle.apps.atal.models import ATALConnector
+from passerelle.base.models import ApiUser, AccessRight
+
+
+@pytest.fixture()
+def connector(db):
+    api = ApiUser.objects.create(username='all', keytype='', key='')
+    connector = ATALConnector.objects.create(
+        base_soap_url='http://example.atal.com/', slug='slug-atal')
+    obj_type = ContentType.objects.get_for_model(connector)
+    AccessRight.objects.create(
+        codename='can_access', apiuser=api, resource_type=obj_type, resource_pk=connector.pk)
+    return connector
+
+
+def mock_atal_soap_call(monkeypatch, return_value=None, side_effect=None):
+    kwargs = {}
+    if return_value is not None:
+        kwargs['return_value'] = return_value
+    if side_effect is not None:
+        kwargs['side_effect'] = side_effect
+    mock_soap_call = mock.Mock(**kwargs)
+    monkeypatch.setattr(ATALConnector, '_soap_call', mock_soap_call)
+    return mock_soap_call
+
+
+class SoapElem(object):
+
+    def __init__(self, **kwargs):
+        for attr, value in kwargs.items():
+            setattr(self, attr, value)
+
+
+REFS = [
+    SoapElem(code='code1', libelle='elem1'), SoapElem(code='code2', libelle='elem2')
+]
+
+
+def test_get_type_activite(app, connector, monkeypatch):
+    mock_soap_call = mock_atal_soap_call(monkeypatch, return_value=REFS)
+    response = app.get('/atal/slug-atal/get_type_activite')
+    assert response.json == {
+        'err': 0,
+        'data': [
+            {'text': 'elem1', 'id': 'code1'},
+            {'text': 'elem2', 'id': 'code2'}]
+    }
+    call_params = mock_soap_call.call_args.kwargs
+    assert call_params['wsdl'] == 'VilleAgileService'
+    assert call_params['method'] == 'getTypeActivite'
+
+
+def test_get_type_de_voie(app, connector, monkeypatch):
+    mock_soap_call = mock_atal_soap_call(monkeypatch, return_value=REFS)
+    response = app.get('/atal/slug-atal/get_type_de_voie')
+    assert response.json == {
+        'err': 0,
+        'data': [
+            {'text': 'elem1', 'id': 'code1'},
+            {'text': 'elem2', 'id': 'code2'}]
+    }
+    call_params = mock_soap_call.call_args.kwargs
+    assert call_params['wsdl'] == 'VilleAgileService'
+    assert call_params['method'] == 'getTypeDeVoie'
+
+
+def test_get_types_equipement(app, connector, monkeypatch):
+    return_value = u"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+    <types>
+        <type id="2" label="Espaces Verts"></type>
+        <type id="4" label="Voirie">
+    </type>
+</types>
+"""
+    mock_soap_call = mock_atal_soap_call(monkeypatch, return_value=return_value)
+    response = app.get('/atal/slug-atal/get_types_equipement')
+    assert response.json == {
+        'err': 0,
+        'data': [
+            {'text': 'Espaces Verts', 'id': '2'},
+            {'text': 'Voirie', 'id': '4'}]
+    }
+    call_params = mock_soap_call.call_args.kwargs
+    assert call_params['wsdl'] == 'VilleAgileService'
+    assert call_params['method'] == 'getTypesEquipement'
+
+
+def test_insert_demande_by_type(app, connector, monkeypatch):
+    mock_soap_call = mock_atal_soap_call(monkeypatch, return_value='DIT19050001')
+    params = {
+        'contact_nom': 'John Doe',
+        'contact_telephone': '0101010101',
+        'contact_email': 'john@doe.com',
+        'contact_adresse': '1 doe street',
+        'demande_objet': 'sarah connor',
+        'demande_lieu': 'LA',
+        'demande_description': 'poker face',
+        'remote_adresse': 'hollywood bd',
+        'code_equipement': 'MAC10',
+        'code_service_demandeur': 'skynet',
+        'date_souhaite': 'now',
+        'type_demande': 'scary'
+    }
+    response = app.post_json('/atal/slug-atal/insert_demande_by_type', params=params)
+    assert response.json == {
+        'err': 0,
+        'data': {'demande_number': 'DIT19050001'}
+    }
+    call_params = mock_soap_call.call_args.kwargs
+    assert call_params['wsdl'] == 'DemandeService'
+    assert call_params['method'] == 'insertDemandeByType'
+    assert call_params['contactNom'] == 'John Doe'
+    assert call_params['contactTelephone'] == '0101010101'
+    assert call_params['contactCourriel'] == 'john@doe.com'
+    assert call_params['contactAdresse'] == '1 doe street'
+    assert call_params['demandeObjet'] == 'sarah connor'
+    assert call_params['demandeLieu'] == 'LA'
+    assert call_params['demandeDescription'] == 'poker face'
+    assert call_params['remoteAddress'] == 'hollywood bd'
+    assert call_params['codeEquipement'] == 'MAC10'
+    assert call_params['codeServiceDemandeur'] == 'skynet'
+    assert call_params['dateSouhaitee'] == 'now'
+    assert call_params['typeDemande'] == 'scary'
+
+
+def test_retrieve_details_demande(app, connector, monkeypatch):
+    mock_soap_call = mock_atal_soap_call(
+        monkeypatch, return_value=dict(code='code1', libelle='elem1'))
+    response = app.get('/atal/slug-atal/retrieve_details_demande/DIT19050001/')
+    assert response.json == {
+        'err': 0,
+        'data': {'code': 'code1', 'libelle': 'elem1'}
+    }
+    call_params = mock_soap_call.call_args.kwargs
+    assert call_params['wsdl'] == 'DemandeService'
+    assert call_params['method'] == 'retrieveDetailsDemande'
+    assert call_params['demandeNumberParam'] == 'DIT19050001'
-- 
2.20.1

