Projet

Général

Profil

0001-sigerly-add-sigerly-connector-47856.patch

Nicolas Roche, 20 novembre 2020 11:50

Télécharger (21,9 ko)

Voir les différences:

Subject: [PATCH] sigerly: add sigerly connector (#47856)

 .../sigerly/migrations/0001_initial.py        |  37 ++++
 .../contrib/sigerly/migrations/__init__.py    |   0
 passerelle/contrib/sigerly/models.py          | 162 ++++++++++++++++
 tests/data/sigerly/getIntervention_1.json     |  56 ++++++
 tests/data/sigerly/getIntervention_2.json     |  91 +++++++++
 tests/settings.py                             |   1 +
 tests/test_sigerly.py                         | 178 ++++++++++++++++++
 7 files changed, 525 insertions(+)
 create mode 100644 passerelle/contrib/sigerly/migrations/0001_initial.py
 create mode 100644 passerelle/contrib/sigerly/migrations/__init__.py
 create mode 100644 passerelle/contrib/sigerly/models.py
 create mode 100644 tests/data/sigerly/getIntervention_1.json
 create mode 100644 tests/data/sigerly/getIntervention_2.json
 create mode 100644 tests/test_sigerly.py
passerelle/contrib/sigerly/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-10-19 13:26
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    initial = True
11

  
12
    dependencies = [
13
        ('base', '0022_auto_20200715_1033'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='Sigerly',
19
            fields=[
20
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21
                ('title', models.CharField(max_length=50, verbose_name='Title')),
22
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
23
                ('description', models.TextField(verbose_name='Description')),
24
                ('basic_auth_username', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication username')),
25
                ('basic_auth_password', models.CharField(blank=True, max_length=128, verbose_name='Basic authentication password')),
26
                ('client_certificate', models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS client certificate')),
27
                ('trusted_certificate_authorities', models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs')),
28
                ('verify_cert', models.BooleanField(default=True, verbose_name='TLS verify certificates')),
29
                ('http_proxy', models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy')),
30
                ('base_url', models.CharField(help_text='example: https://sig.sigerly.fr/syecl_intervention/webservicev2/', max_length=256, verbose_name='Service URL')),
31
                ('users', models.ManyToManyField(blank=True, related_name='_sigerly_users_+', related_query_name='+', to='base.ApiUser')),
32
            ],
33
            options={
34
                'verbose_name': 'Sigerly',
35
            },
36
        ),
37
    ]
passerelle/contrib/sigerly/models.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  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 urllib.parse import urljoin
18

  
19
from django.db import models
20
from django.utils.translation import ugettext_lazy as _
21

  
22
from passerelle.base.models import BaseResource, HTTPResource
23
from passerelle.utils.api import endpoint
24
from passerelle.utils.jsonresponse import APIError
25

  
26
CREATE_SCHEMA = {
27
    '$schema': 'http://json-schema.org/draft-04/schema#',
28
    "type": "object",
29
    'required': ['demandeur', 'id_typeinterv', 'id_urgence', 'elements'],
30
    'properties': {
31
        'demandeur': {
32
            'description': "Nom du demandeur",
33
            'type': 'string',
34
        },
35
        'id_typeinterv': {
36
            'description': "Type de l'intervention",
37
            'type': 'string',
38
        },
39
        'id_urgence': {
40
            'description': 'Urgence',
41
            'type': 'string',
42
        },
43
        'id_qualification': {
44
            'description': 'Qualification',
45
            'type': 'string',
46
        },
47
        'observations': {
48
            'description': 'Observations',
49
            'type': 'string',
50
        },
51
        'elements': {
52
            'description': "Identifiant de l'objet : liste séparée par ':'",
53
            'type': 'string',
54
            'pattern': r'^[0-9A-Z :]+$'
55
        },
56
    }
57
}
58

  
59
QUERY_SCHEMA = {
60
    '$schema': 'http://json-schema.org/draft-04/schema#',
61
    "type": "object",
62
    'properties': {
63
        'id_intervention': {
64
            'description': 'Rechercher une intervention par son numéro' \
65
            ' (non cumulable avec les autres filtres)',
66
            'type': 'string',
67
        },
68
        'date_debut_demande': {
69
            'description': 'Recherche toutes les interventions dont la date de demande' \
70
            ' est supérieure ou égale à la date renseignée (YYYY-MM-DD)',
71
            'type': 'string',
72
        },
73
        'date_fin_demande': {
74
            'description': 'Recherche toutes les interventions dont la date de demande' \
75
            ' est inférieure ou égale à la date renseignée (YYYY-MM-DD)',
76
            'type': 'string',
77
        },
78
        'insee': {
79
            'description': "Code insee de la commune : liste séparée par ':'",
80
            'type': 'string',
81
            'pattern': r'^[0-9A-Z :]+$'
82
        },
83
    }
84
}
85

  
86

  
87
class Sigerly(BaseResource, HTTPResource):
88
    base_url = models.CharField(
89
        max_length=256, blank=False,
90
        verbose_name=_('Service URL'),
91
        help_text=_('example: https://sig.sigerly.fr/syecl_intervention_preprod/webservicev2/')
92
    )
93

  
94
    category = _('Business Process Connectors')
95

  
96
    class Meta:
97
        verbose_name = _('Sigerly')
98

  
99
    def request(self, uri, json):
100
        url = urljoin(self.base_url, uri)
101
        headers = {'Accept': 'application/json'}
102

  
103
        response = self.requests.post(url, json=json, headers=headers)
104

  
105
        if response.status_code // 100 != 2:
106
            try:
107
                json_content = response.json()
108
            except ValueError:
109
                json_content = None
110
            raise APIError('error status:%s %r, content:%r' %
111
                           (response.status_code, response.reason, response.content[:1024]),
112
                           data={'status_code': response.status_code,
113
                                 'json_content': json_content})
114
        try:
115
            json_response = response.json()
116
        except ValueError:
117
            raise APIError('invalid JSON content:%r' % response.content[:1024])
118
        if 'success' in json_response and not json_response['success']:
119
            raise APIError(json_response.get('message', 'No message specified'))
120
        return json_response
121

  
122
    @endpoint(
123
        display_order=1,
124
        perm='can_access', methods=['post'],
125
        description=_('Versement d’une demande d’éclairage public dans l’extranet du SIGERLY'),
126
        post={'request_body': {'schema': {'application/json': CREATE_SCHEMA}}})
127
    def create(self, request, post_data):
128
        post_data['id_typeinterv'] = int(post_data['id_typeinterv'])
129
        post_data['id_urgence'] = int(post_data['id_urgence'])
130
        post_data['id_qualification'] = int(post_data['id_qualification'])
131
        post_data['elements'] = [
132
            x.strip() for x in post_data['elements'].split(':') if x.strip()]
133

  
134
        response = self.request('createIntervention.php', json=post_data)
135
        if not response.get('success', None):
136
            raise APIError(response.get('message', None))
137
        if not response.get('message', None):
138
            raise APIError('No intervention id returned')
139
        return {'data': response}
140

  
141
    @endpoint(
142
        display_order=2,
143
        perm='can_access', methods=['post'],
144
        description=_('Interroger le SIGERLY sur l’etat d’avancement d’une demande'),
145
        post={'request_body': {'schema': {'application/json': QUERY_SCHEMA}}})
146
    def query(self, request, post_data):
147
        if post_data.get('id_intervention', None):
148
            post_data['id_intervention'] = int(post_data['id_intervention'])
149
            post_data.pop('date_debut_demande', None)
150
            post_data.pop('date_fin_demande', None)
151
            post_data.pop('insee', None)
152
        else:
153
            post_data.pop('id_intervention', None)
154
            if post_data.get('insee'):
155
                post_data['insee'] = [
156
                    x.strip() for x in post_data['insee'].split(':') if x.strip()]
157

  
158
        response = self.request('getIntervention.php', json=post_data)
159
        for record in response:
160
            record['id'] = record.get('code_inter')
161
            record['text'] = '%(code_inter)s: %(libelle_intervention)s' % record
162
        return {'data': response}
tests/data/sigerly/getIntervention_1.json
1
[
2
   {
3
      "annule" : null,
4
      "code_inter" : "DP.20.116.1",
5
      "date_cr1" : null,
6
      "date_emission" : "2020-11-19",
7
      "date_inter_planif1" : null,
8
      "date_inter_rea_1" : null,
9
      "date_valid" : "2020-11-19",
10
      "date_valid_planif_1" : null,
11
      "date_validfact" : null,
12
      "elements" : [
13
         {
14
            "icon_gmap" : "luminaire.png",
15
            "id_interv" : 22014,
16
            "idelement" : 42282,
17
            "ident" : "LIMW003D",
18
            "libelle_objetgeo" : "Luminaire",
19
            "nom_rue" : "Voie d'accès parc des Sports",
20
            "the_geom" : "0101000020110F000038A23D18A5372041C775D7FE5BF35541",
21
            "travaux" : []
22
         },
23
         {
24
            "icon_gmap" : "luminaire.png",
25
            "id_interv" : 22014,
26
            "idelement" : 42283,
27
            "ident" : "LIMW003C",
28
            "libelle_objetgeo" : "Luminaire",
29
            "nom_rue" : "Voie d'accès parc des Sports",
30
            "the_geom" : "0101000020110F00006F56132FA53720417E4630FB5BF35541",
31
            "travaux" : []
32
         }
33
      ],
34
      "flag_mob" : null,
35
      "garantie" : null,
36
      "id_commune" : 29,
37
      "id_demande_web" : 10914,
38
      "id_entreprise" : 2,
39
      "id_entreprise_2" : null,
40
      "id_traitement" : null,
41
      "id_typeinterv" : 5,
42
      "id_urgence" : 1,
43
      "idinterv" : 22014,
44
      "libcause" : null,
45
      "libcommune" : "LIMONEST",
46
      "libelle_intervention" : "DEPANNAGE EP",
47
      "libelle_urgence" : "Normale",
48
      "libentreprise" : "EIFFAGE",
49
      "libentreprise2" : null,
50
      "libterritoire" : "NORD",
51
      "num_mandat" : null,
52
      "ref_technique" : null,
53
      "valid_entreprise" : null,
54
      "valid_sydev" : null
55
   }
56
]
tests/data/sigerly/getIntervention_2.json
1
[
2
   {
3
      "annule" : null,
4
      "code_inter" : "VN.20.291.11",
5
      "date_cr1" : null,
6
      "date_emission" : "2020-11-19",
7
      "date_inter_planif1" : null,
8
      "date_inter_rea_1" : null,
9
      "date_inter_rea_2" : null,
10
      "date_valid" : null,
11
      "date_valid_planif_1" : null,
12
      "date_valid_planif_2" : null,
13
      "date_validfact" : null,
14
      "elements" : [],
15
      "flag_mob" : null,
16
      "garantie" : null,
17
      "id_commune" : 6,
18
      "id_demande_web" : null,
19
      "id_entreprise" : 5,
20
      "id_entreprise_2" : 6,
21
      "id_suite" : null,
22
      "id_traitement" : 1,
23
      "id_typeinterv" : 4,
24
      "id_urgence" : null,
25
      "idinterv" : 25080,
26
      "inseecommune" : "069291",
27
      "libcause" : null,
28
      "libcommune" : "ST SYMPHORIEN D OZON",
29
      "libelle_intervention" : "VISITE DE NUIT",
30
      "libelle_urgence" : null,
31
      "libentreprise" : "SERPOLLET",
32
      "libentreprise2" : "INEO",
33
      "libterritoire" : "SUD",
34
      "mail" : "Mikael.BOBROSKY@serpollet.com",
35
      "num_mandat" : null,
36
      "ref_technique" : null,
37
      "toodego" : null,
38
      "valid_entreprise" : null,
39
      "valid_sydev" : null
40
   },
41
   {
42
      "annule" : null,
43
      "code_inter" : "DP.20.291.53",
44
      "date_cr1" : "2020-11-19",
45
      "date_emission" : "2020-11-19",
46
      "date_inter_planif1" : "2020-11-19",
47
      "date_inter_rea_1" : "2020-11-19",
48
      "date_inter_rea_2" : "2020-11-19",
49
      "date_valid" : "2020-11-19",
50
      "date_valid_planif_1" : "19/11/2020",
51
      "date_valid_planif_2" : "2020-11-19",
52
      "date_validfact" : null,
53
      "elements" : [
54
         {
55
            "icon_gmap" : "armoire.png",
56
            "id_interv" : 27948,
57
            "idelement" : 51681,
58
            "ident" : "AB",
59
            "libelle_objetgeo" : "Armoire",
60
            "nom_rue" : "Portes de Lyon (avenue des)",
61
            "the_geom" : "0101000020110F0000C7A520BC497F20415B9A1EFB2AD45541",
62
            "travaux" : []
63
         }
64
      ],
65
      "flag_mob" : null,
66
      "garantie" : null,
67
      "id_commune" : 6,
68
      "id_demande_web" : 15879,
69
      "id_entreprise" : 5,
70
      "id_entreprise_2" : null,
71
      "id_suite" : 2,
72
      "id_traitement" : 1,
73
      "id_typeinterv" : 5,
74
      "id_urgence" : null,
75
      "idinterv" : 27948,
76
      "inseecommune" : "069291",
77
      "libcause" : null,
78
      "libcommune" : "ST SYMPHORIEN D OZON",
79
      "libelle_intervention" : "DEPANNAGE EP",
80
      "libelle_urgence" : null,
81
      "libentreprise" : "SERPOLLET",
82
      "libentreprise2" : null,
83
      "libterritoire" : "SUD",
84
      "mail" : "Mikael.BOBROSKY@serpollet.com",
85
      "num_mandat" : null,
86
      "ref_technique" : null,
87
      "toodego" : null,
88
      "valid_entreprise" : 1,
89
      "valid_sydev" : 1
90
   }
91
]
tests/settings.py
23 23
    'passerelle.contrib.greco',
24 24
    'passerelle.contrib.grenoble_gru',
25 25
    'passerelle.contrib.iparapheur',
26 26
    'passerelle.contrib.iws',
27 27
    'passerelle.contrib.lille_urban_card',
28 28
    'passerelle.contrib.mdph13',
29 29
    'passerelle.contrib.nancypoll',
30 30
    'passerelle.contrib.planitech',
31
    'passerelle.contrib.sigerly',
31 32
    'passerelle.contrib.solis_apa',
32 33
    'passerelle.contrib.solis_afi_mss',
33 34
    'passerelle.contrib.strasbourg_eu',
34 35
    'passerelle.contrib.stub_invoices',
35 36
    'passerelle.contrib.teamnet_axel',
36 37
    'passerelle.contrib.tcl',
37 38
    'passerelle.contrib.toulouse_axel',
38 39
    'passerelle.contrib.lille_kimoce',
tests/test_sigerly.py
1
# passerelle - uniform access to multiple data sources and services
2
# Copyright (C) 2020  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
import json
18
import os
19

  
20
import mock
21
import pytest
22

  
23
import utils
24

  
25
from passerelle.contrib.sigerly.models import Sigerly
26
from passerelle.utils.jsonresponse import APIError
27

  
28
@pytest.fixture
29
def connector(db):
30
    return utils.setup_access_rights(Sigerly.objects.create(
31
        slug='test',
32
        base_url='https://dummy-server.org'
33
    ))
34

  
35

  
36
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'sigerly')
37

  
38
def json_get_data(filename):
39
    with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as fd:
40
        return json.dumps(json.load(fd))
41

  
42
def response(status_code, content):
43
    return utils.FakedResponse(content=content, status_code=status_code)
44

  
45
CREATE = response(200, json.dumps({
46
    'success': True,
47
    'message': '7830',  # unrelated id
48
}))
49

  
50
CREATE_ERROR_1 = response(200, json.dumps({
51
    'success': False,
52
    'message': 'XXX',  # error message
53
}))
54

  
55
CREATE_ERROR_2 = response(200, json.dumps({
56
    'success': True,
57
    'message': '',
58
}))
59

  
60

  
61
QUERY_1 = response(200, json_get_data('getIntervention_1'))
62
QUERY_2 = response(200, json_get_data('getIntervention_2'))
63

  
64

  
65
def get_endpoint(name):
66
    return utils.generic_endpoint_url('sigerly', name)
67

  
68

  
69
@mock.patch('passerelle.utils.Request.post')
70
@pytest.mark.parametrize('status_code, json_content, a_dict', [
71
    (200, 'not json', None),
72
    (500, '{"message": "help"}', {'message': 'help'}),
73
    (500, 'not json', None),
74
])
75
def test_request_error(mocked_post, app, connector, status_code, json_content, a_dict):
76
    mocked_post.side_effect = [response(status_code, json_content)]
77
    with pytest.raises(APIError) as exc:
78
        connector.request('some-url', json=None)
79
    assert exc.value.err
80
    if status_code == 200:
81
        assert exc.value.http_status == 200
82
        assert exc.value.args[0] == "invalid JSON content:'%s'" % json_content
83
    else:
84
        assert exc.value.data['status_code'] == status_code
85
        assert exc.value.data['json_content'] == a_dict
86

  
87

  
88
@mock.patch('passerelle.utils.Request.post')
89
def test_create(mocked_post, app, connector):
90
    mocked_post.side_effect = [CREATE]
91
    endpoint = get_endpoint('create')
92
    payload = {
93
        'demandeur': 'Test webservice',
94
        'id_typeinterv': '5',
95
        'id_urgence': '1',
96
        'id_qualification': '8',
97
        'observations': 'Test webservice',
98
        'elements': 'LIMW003D:LIMWW003C',
99
    }
100
    resp = app.post_json(endpoint, params=payload)
101
    assert mocked_post.mock_calls == [mock.call(
102
        'https://dummy-server.org/createIntervention.php',
103
        headers={'Accept': 'application/json'},
104
        json={
105
            'demandeur': 'Test webservice',
106
            'id_typeinterv': 5,
107
            'id_urgence': 1,
108
            'id_qualification': 8,
109
            'observations': 'Test webservice',
110
            'elements': ['LIMW003D', 'LIMWW003C'],
111
        })]
112
    assert resp.json == {'err': 0, 'data': json.loads(CREATE.content)}
113

  
114

  
115
@mock.patch('passerelle.utils.Request.post')
116
@pytest.mark.parametrize('response, desc', [
117
    (CREATE_ERROR_1, 'XXX'),
118
    (CREATE_ERROR_2, 'No intervention id returned'),
119
])
120
def test_create_error(mocked_post, app, connector, response, desc):
121
    mocked_post.side_effect = [response]
122
    endpoint = get_endpoint('create')
123
    payload = {
124
        'demandeur': 'Test webservice',
125
        'id_typeinterv': '5',
126
        'id_urgence': '1',
127
        'id_qualification': '8',
128
        'observations': 'Test webservice',
129
        'elements': 'LIMW003D:LIMWW003C',
130
    }
131
    resp = app.post_json(endpoint, params=payload)
132
    assert resp.json['err']
133
    assert resp.json['err_desc'] == desc
134

  
135

  
136
@mock.patch('passerelle.utils.Request.post')
137
def test_query_id(mocked_post, app, connector):
138
    mocked_post.return_value = QUERY_1
139
    endpoint = get_endpoint('query')
140
    payload = {
141
        'id_intervention': '10914',
142
    }
143
    resp = app.post_json(endpoint, params=payload)
144
    assert mocked_post.mock_calls[0] == mock.call(
145
        'https://dummy-server.org/getIntervention.php',
146
        headers={'Accept': 'application/json'},
147
        json={'id_intervention': 10914})
148
    assert not resp.json['err']
149
    assert len(resp.json['data']) == 1
150
    assert resp.json['data'][0]['id_demande_web'] == 10914
151
    assert resp.json['data'][0]['idinterv'] == 22014
152
    elements = resp.json['data'][0]['elements']
153
    assert len(elements) == 2
154
    assert [x['ident'] for x in elements] == ['LIMW003D', 'LIMW003C']
155

  
156

  
157
@mock.patch('passerelle.utils.Request.post')
158
def test_query_filters(mocked_post, app, connector):
159
    mocked_post.return_value = QUERY_2
160
    endpoint = get_endpoint('query')
161
    payload = {
162
        'date_debut_demande': '19/11/2020',
163
        'date_fin_demande': '19/11/2020',
164
        'insee': '069291:069283'
165
    }
166
    resp = app.post_json(endpoint, params=payload)
167
    assert mocked_post.mock_calls[0] == mock.call(
168
        'https://dummy-server.org/getIntervention.php',
169
        headers={'Accept': 'application/json'},
170
        json={
171
            'date_debut_demande': '19/11/2020',
172
            'date_fin_demande': '19/11/2020',
173
            'insee': ['069291', '069283'],
174
        })
175
    assert not resp.json['err']
176
    assert len(resp.json['data']) == 2
177
    assert [x['date_emission'] for x in resp.json['data']] == ['2020-11-19'] * 2
178
    assert [x['inseecommune'] for x in resp.json['data']] == ['069291'] * 2
0
-