Projet

Général

Profil

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

Nicolas Roche, 11 février 2021 09:30

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                         | 201 ++++++++++++++++++
 7 files changed, 548 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,
90
        blank=False,
91
        verbose_name='URL du webservice',
92
        help_text=_('exemple: https://sig.sigerly.fr/syecl_intervention_preprod/webservicev2/'),
93
    )
94

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

  
97
    class Meta:
98
        verbose_name = 'Sigerly'
99

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

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

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

  
122
    @endpoint(
123
        perm='can_access',
124
        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
    )
128
    def create(self, request, post_data):
129
        post_data['id_typeinterv'] = int(post_data['id_typeinterv'])
130
        post_data['id_urgence'] = int(post_data['id_urgence'])
131
        post_data['id_qualification'] = int(post_data['id_qualification'])
132
        post_data['elements'] = [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
        perm='can_access',
143
        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
    )
147
    def query(self, request, post_data):
148
        if post_data.get('id_intervention', None):
149
            post_data['id_intervention'] = int(post_data['id_intervention'])
150
            post_data.pop('date_debut_demande', None)
151
            post_data.pop('date_fin_demande', None)
152
            post_data.pop('insee', None)
153
        else:
154
            post_data.pop('id_intervention', None)
155
            if post_data.get('insee'):
156
                post_data['insee'] = [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
25 25
    'passerelle.contrib.isere_ens',
26 26
    'passerelle.contrib.iparapheur',
27 27
    'passerelle.contrib.iws',
28 28
    'passerelle.contrib.lille_urban_card',
29 29
    'passerelle.contrib.mdph13',
30 30
    'passerelle.contrib.nancypoll',
31 31
    'passerelle.contrib.planitech',
32 32
    'passerelle.contrib.rsa13',
33
    'passerelle.contrib.sigerly',
33 34
    'passerelle.contrib.solis_apa',
34 35
    'passerelle.contrib.solis_afi_mss',
35 36
    'passerelle.contrib.strasbourg_eu',
36 37
    'passerelle.contrib.stub_invoices',
37 38
    'passerelle.contrib.teamnet_axel',
38 39
    'passerelle.contrib.tcl',
39 40
    'passerelle.contrib.toulouse_axel',
40 41
    '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

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

  
33

  
34
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'sigerly')
35

  
36

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

  
41

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

  
45

  
46
CREATE = response(
47
    200,
48
    json.dumps(
49
        {
50
            'success': True,
51
            'message': '7830',  # unrelated id
52
        }
53
    ),
54
)
55

  
56
CREATE_ERROR_1 = response(
57
    200,
58
    json.dumps(
59
        {
60
            'success': False,
61
            'message': 'XXX',  # error message
62
        }
63
    ),
64
)
65

  
66
CREATE_ERROR_2 = response(
67
    200,
68
    json.dumps(
69
        {
70
            'success': True,
71
            'message': '',
72
        }
73
    ),
74
)
75

  
76

  
77
QUERY_1 = response(200, json_get_data('getIntervention_1'))
78
QUERY_2 = response(200, json_get_data('getIntervention_2'))
79

  
80

  
81
def get_endpoint(name):
82
    return utils.generic_endpoint_url('sigerly', name)
83

  
84

  
85
@mock.patch('passerelle.utils.Request.post')
86
@pytest.mark.parametrize(
87
    'status_code, json_content, a_dict',
88
    [
89
        (200, 'not json', None),
90
        (500, '{"message": "help"}', {'message': 'help'}),
91
        (500, 'not json', None),
92
    ],
93
)
94
def test_request_error(mocked_post, app, connector, status_code, json_content, a_dict):
95
    mocked_post.side_effect = [response(status_code, json_content)]
96
    with pytest.raises(APIError) as exc:
97
        connector.request('some-url', json=None)
98
    assert exc.value.err
99
    if status_code == 200:
100
        assert exc.value.http_status == 200
101
        assert exc.value.args[0] == "invalid JSON content:'%s'" % json_content
102
    else:
103
        assert exc.value.data['status_code'] == status_code
104
        assert exc.value.data['json_content'] == a_dict
105

  
106

  
107
@mock.patch('passerelle.utils.Request.post')
108
def test_create(mocked_post, app, connector):
109
    mocked_post.side_effect = [CREATE]
110
    endpoint = get_endpoint('create')
111
    payload = {
112
        'demandeur': 'Test webservice',
113
        'id_typeinterv': '5',
114
        'id_urgence': '1',
115
        'id_qualification': '8',
116
        'observations': 'Test webservice',
117
        'elements': 'LIMW003D:LIMWW003C',
118
    }
119
    resp = app.post_json(endpoint, params=payload)
120
    assert mocked_post.mock_calls == [
121
        mock.call(
122
            'https://dummy-server.org/createIntervention.php',
123
            headers={'Accept': 'application/json'},
124
            json={
125
                'demandeur': 'Test webservice',
126
                'id_typeinterv': 5,
127
                'id_urgence': 1,
128
                'id_qualification': 8,
129
                'observations': 'Test webservice',
130
                'elements': ['LIMW003D', 'LIMWW003C'],
131
            },
132
        )
133
    ]
134
    assert resp.json == {'err': 0, 'data': json.loads(CREATE.content)}
135

  
136

  
137
@mock.patch('passerelle.utils.Request.post')
138
@pytest.mark.parametrize(
139
    'response, desc',
140
    [
141
        (CREATE_ERROR_1, 'XXX'),
142
        (CREATE_ERROR_2, 'No intervention id returned'),
143
    ],
144
)
145
def test_create_error(mocked_post, app, connector, response, desc):
146
    mocked_post.side_effect = [response]
147
    endpoint = get_endpoint('create')
148
    payload = {
149
        'demandeur': 'Test webservice',
150
        'id_typeinterv': '5',
151
        'id_urgence': '1',
152
        'id_qualification': '8',
153
        'observations': 'Test webservice',
154
        'elements': 'LIMW003D:LIMWW003C',
155
    }
156
    resp = app.post_json(endpoint, params=payload)
157
    assert resp.json['err']
158
    assert resp.json['err_desc'] == desc
159

  
160

  
161
@mock.patch('passerelle.utils.Request.post')
162
def test_query_id(mocked_post, app, connector):
163
    mocked_post.return_value = QUERY_1
164
    endpoint = get_endpoint('query')
165
    payload = {
166
        'id_intervention': '10914',
167
    }
168
    resp = app.post_json(endpoint, params=payload)
169
    assert mocked_post.mock_calls[0] == mock.call(
170
        'https://dummy-server.org/getIntervention.php',
171
        headers={'Accept': 'application/json'},
172
        json={'id_intervention': 10914},
173
    )
174
    assert not resp.json['err']
175
    assert len(resp.json['data']) == 1
176
    assert resp.json['data'][0]['id_demande_web'] == 10914
177
    assert resp.json['data'][0]['idinterv'] == 22014
178
    elements = resp.json['data'][0]['elements']
179
    assert len(elements) == 2
180
    assert [x['ident'] for x in elements] == ['LIMW003D', 'LIMW003C']
181

  
182

  
183
@mock.patch('passerelle.utils.Request.post')
184
def test_query_filters(mocked_post, app, connector):
185
    mocked_post.return_value = QUERY_2
186
    endpoint = get_endpoint('query')
187
    payload = {'date_debut_demande': '19/11/2020', 'date_fin_demande': '19/11/2020', 'insee': '069291:069283'}
188
    resp = app.post_json(endpoint, params=payload)
189
    assert mocked_post.mock_calls[0] == mock.call(
190
        'https://dummy-server.org/getIntervention.php',
191
        headers={'Accept': 'application/json'},
192
        json={
193
            'date_debut_demande': '19/11/2020',
194
            'date_fin_demande': '19/11/2020',
195
            'insee': ['069291', '069283'],
196
        },
197
    )
198
    assert not resp.json['err']
199
    assert len(resp.json['data']) == 2
200
    assert [x['date_emission'] for x in resp.json['data']] == ['2020-11-19'] * 2
201
    assert [x['inseecommune'] for x in resp.json['data']] == ['069291'] * 2
0
-