Projet

Général

Profil

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

Nicolas Roche, 19 février 2021 09:58

Télécharger (20,8 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          | 154 +++++++++++++++++
 tests/data/sigerly/getIntervention_1.json     |  56 +++++++
 tests/data/sigerly/getIntervention_2.json     |  91 ++++++++++
 tests/settings.py                             |   1 +
 tests/test_sigerly.py                         | 157 ++++++++++++++++++
 7 files changed, 496 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) 2021  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

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

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

  
87

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

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

  
98
    class Meta:
99
        verbose_name = 'Sigerly'
100

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

  
105
        response = self.requests.post(url, json=json, headers=headers)
106
        if not response.ok:
107
            raise APIError('Sigerly returned bad response code from API: %s' % response.status_code)
108
        try:
109
            json_response = response.json()
110
        except ValueError:
111
            raise APIError('Sigerly returned invalid JSON content: %r' % response.content[:1024])
112
        return json_response
113

  
114
    @endpoint(
115
        perm='can_access',
116
        methods=['post'],
117
        description='Envoyer une demande',
118
        post={'request_body': {'schema': {'application/json': CREATE_SCHEMA}}},
119
    )
120
    def create(self, request, post_data):
121
        post_data['id_typeinterv'] = int(post_data['id_typeinterv'])
122
        post_data['id_urgence'] = int(post_data['id_urgence'])
123
        post_data['id_qualification'] = int(post_data['id_qualification'])
124
        post_data['elements'] = [x.strip() for x in post_data['elements'].split(':') if x.strip()]
125

  
126
        response = self.request('createIntervention.php', json=post_data)
127
        if not response.get('success', None):
128
            raise APIError(response.get('message', None))
129
        if not response.get('message', None):
130
            raise APIError('No intervention id returned')
131
        return {'data': response}
132

  
133
    @endpoint(
134
        perm='can_access',
135
        methods=['post'],
136
        description='Récupérer le statut d’une demande',
137
        post={'request_body': {'schema': {'application/json': QUERY_SCHEMA}}},
138
    )
139
    def query(self, request, post_data):
140
        if post_data.get('id_intervention', None):
141
            post_data['id_intervention'] = int(post_data['id_intervention'])
142
            post_data.pop('date_debut_demande', None)
143
            post_data.pop('date_fin_demande', None)
144
            post_data.pop('insee', None)
145
        else:
146
            post_data.pop('id_intervention', None)
147
            if post_data.get('insee'):
148
                post_data['insee'] = [x.strip() for x in post_data['insee'].split(':') if x.strip()]
149

  
150
        response = self.request('getIntervention.php', json=post_data)
151
        for record in response:
152
            record['id'] = record.get('code_inter')
153
            record['text'] = '%(code_inter)s: %(libelle_intervention)s' % record
154
        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) 2021  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 httmock
21
import pytest
22

  
23
from passerelle.contrib.sigerly.models import Sigerly
24

  
25
import utils
26

  
27

  
28
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'sigerly')
29

  
30

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

  
35

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

  
40

  
41
def get_endpoint(name):
42
    return utils.generic_endpoint_url('sigerly', name)
43

  
44

  
45
@pytest.mark.parametrize(
46
    'status_code, content, err_desc',
47
    [
48
        (500, 'xxx', 'bad response code from API: 500'),
49
        (400, 'xxx', 'bad response code from API: 400'),
50
        (200, 'not json', 'invalid JSON content'),
51
    ],
52
)
53
def test_request_error(app, connector, status_code, content, err_desc):
54
    endpoint = get_endpoint('query')
55

  
56
    @httmock.all_requests
57
    def sigerly_mock(url, request):
58
        return httmock.response(status_code, content)
59

  
60
    with httmock.HTTMock(sigerly_mock):
61
        resp = app.post_json(endpoint, params={})
62

  
63
    assert resp.json['err']
64
    assert err_desc in resp.json['err_desc']
65

  
66

  
67
def test_create(app, connector):
68
    endpoint = get_endpoint('create')
69
    payload = {
70
        'demandeur': 'Test webservice',
71
        'id_typeinterv': '5',
72
        'id_urgence': '1',
73
        'id_qualification': '8',
74
        'observations': 'Test webservice',
75
        'elements': 'LIMW003D:LIMWW003C',
76
    }
77

  
78
    @httmock.urlmatch(netloc='dummy-server.org', path='/createIntervention.php', method='POST')
79
    def sigerly_mock(url, request):
80
        assert request.headers['Accept'] == 'application/json'
81
        assert json.loads(request.body)['elements'] == ['LIMW003D', 'LIMWW003C']
82
        return httmock.response(200, json.dumps({'success': True, 'message': '7830'}))
83

  
84
    with httmock.HTTMock(sigerly_mock):
85
        resp = app.post_json(endpoint, params=payload)
86
    assert not resp.json['err']
87
    assert resp.json['data']['message'] == '7830'  # id unusable within query endpoint
88

  
89

  
90
@pytest.mark.parametrize(
91
    'success, message, err_desc',
92
    [
93
        (False, 'an error message', 'an error message'),
94
        (True, '', 'No intervention id returned'),
95
    ],
96
)
97
def test_create_error(app, connector, success, message, err_desc):
98
    endpoint = get_endpoint('create')
99
    payload = {
100
        'demandeur': 'Test webservice',
101
        'id_typeinterv': '5',
102
        'id_urgence': '1',
103
        'id_qualification': '8',
104
        'observations': 'Test webservice',
105
        'elements': 'LIMW003D:LIMWW003C',
106
    }
107

  
108
    @httmock.urlmatch(netloc='dummy-server.org', path='/createIntervention.php', method='POST')
109
    def sigerly_mock(url, request):
110
        assert request.headers['Accept'] == 'application/json'
111
        return httmock.response(200, json.dumps({'success': success, 'message': message}))
112

  
113
    with httmock.HTTMock(sigerly_mock):
114
        resp = app.post_json(endpoint, params=payload)
115
    assert resp.json['err']
116
    assert resp.json['err_desc'] == err_desc
117

  
118

  
119
def test_query_id(app, connector):
120
    endpoint = get_endpoint('query')
121
    payload = {
122
        'id_intervention': '10914',
123
    }
124

  
125
    @httmock.urlmatch(netloc='dummy-server.org', path='/getIntervention.php', method='POST')
126
    def sigerly_mock(url, request):
127
        assert request.headers['Accept'] == 'application/json'
128
        assert json.loads(request.body) == {'id_intervention': 10914}
129
        return httmock.response(200, json_get_data('getIntervention_1'))
130

  
131
    with httmock.HTTMock(sigerly_mock):
132
        resp = app.post_json(endpoint, params=payload)
133
    assert not resp.json['err']
134
    assert len(resp.json['data']) == 1
135
    assert resp.json['data'][0]['id_demande_web'] == 10914
136
    assert resp.json['data'][0]['idinterv'] == 22014
137
    elements = resp.json['data'][0]['elements']
138
    assert len(elements) == 2
139
    assert [x['ident'] for x in elements] == ['LIMW003D', 'LIMW003C']
140

  
141

  
142
def test_query_filters(app, connector):
143
    endpoint = get_endpoint('query')
144
    payload = {'date_debut_demande': '19/11/2020', 'date_fin_demande': '19/11/2020', 'insee': '::069291:::069283::'}
145

  
146
    @httmock.urlmatch(netloc='dummy-server.org', path='/getIntervention.php', method='POST')
147
    def sigerly_mock(url, request):
148
        assert request.headers['Accept'] == 'application/json'
149
        assert json.loads(request.body)['insee'] == ['069291', '069283']
150
        return httmock.response(200, json_get_data('getIntervention_2'))
151

  
152
    with httmock.HTTMock(sigerly_mock):
153
        resp = app.post_json(endpoint, params=payload)
154
    assert not resp.json['err']
155
    assert len(resp.json['data']) == 2
156
    assert [x['date_emission'] for x in resp.json['data']] == ['2020-11-19'] * 2
157
    assert [x['inseecommune'] for x in resp.json['data']] == ['069291'] * 2
0
-