Projet

Général

Profil

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

Nicolas Roche, 19 octobre 2020 17:52

Télécharger (21,5 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          | 157 +++++++++++++++++
 passerelle/contrib/sigerly/models.py~         |  80 +++++++++
 passerelle/settings.py                        |   1 +
 tests/data/sigerly/getIntervention_1.json     |  48 +++++
 tests/settings.py                             |   1 +
 tests/test_sigerly.py                         | 164 ++++++++++++++++++
 8 files changed, 488 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 passerelle/contrib/sigerly/models.py~
 create mode 100644 tests/data/sigerly/getIntervention_1.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/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
        response = self.requests.post(url, json=json, headers=headers)
103

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

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

  
130
        response = self.request('createIntervention.php/', json=post_data)
131
        if not response.get('success', None):
132
            raise APIError(response.get('message', None))
133
        if not response.get('message', None):
134
            raise APIError('No intervention id returned')
135
        return {'data': response}
136

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

  
153
        response = self.request('getIntervention.php/', json=post_data)
154
        for record in response:
155
            record['id'] = record.get('code_inter')
156
            record['text'] = '%(code_inter)s: %(libelle_intervention)s' % record
157
        return {'data': response}
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
        'observations': {
44
            'description': 'Observations',
45
            'type': 'string',
46
        },
47
        'elements': {
48
            'description': "Identifiant de l'objet : liste séparée par ':'",
49
            'type': 'string',
50
            'pattern': r'^[0-9A-Z :]+$'
51
        },
52
    }
53
}
54

  
55
GET_SCHEMA = {
56
    '$schema': 'http://json-schema.org/draft-04/schema#',
57
    "type": "object",
58
    'properties': {
59
        'id_intervention': {
60
            'description': 'Rechercher une intervention par son numéro' \
61
            ' (non cumulable avec les autres filtres)',
62
            'type': 'string',
63
        },
64
        'date_debut_demande': {
65
            'description': 'Recherche toutes les interventions dont la date de demande' \
66
            ' est supérieure ou égale à la date renseignée (YYYY-MM-DD)',
67
            'type': 'string',
68
        },
69
        'date_fin_demande': {
70
            'description': 'Recherche toutes les interventions dont la date de demande' \
71
            ' est inférieure ou égale à la date renseignée (YYYY-MM-DD)',
72
            'type': 'string',
73
        },
74
        'insee': {
75
            'description': "Code insee de la commune : liste séparée par ':'",
76
            'type': 'string',
77
            'pattern': r'^[0-9A-Z :]+$'
78
        },
79
    }
80
}
passerelle/settings.py
155 155
    'passerelle.apps.ovh',
156 156
    'passerelle.apps.oxyd',
157 157
    'passerelle.apps.phonecalls',
158 158
    'passerelle.apps.solis',
159 159
    'passerelle.apps.twilio',
160 160
    'passerelle.apps.vivaticket',
161 161
    # backoffice templates and static
162 162
    'gadjo',
163
    'passerelle.contrib.sigerly',  # testing purpose, to remove
163 164
)
164 165

  
165 166
# disable some applications for now
166 167
PASSERELLE_APP_BDP_ENABLED = False
167 168
PASSERELLE_APP_GDC_ENABLED = False
168 169
PASSERELLE_APP_STRASBOURG_EU_ENABLED = False
169 170

  
170 171
# Authentication settings
tests/data/sigerly/getIntervention_1.json
1
[
2
   {
3
      "code_inter" : "DP.20.003.5",
4
      "date_cr1" : "2020-02-27",
5
      "date_emission" : "2020-02-10",
6
      "date_inter_plannif1" : "2020-02-20",
7
      "date_inter_plannif2" : "2020-02-25",
8
      "date_valid" : "2020-02-18",
9
      "elements" : [
10
         {
11
            "ident" : "ALBE009A",
12
            "libelle_objetgeo" : "Luminaire",
13
            "travaux" : [
14
               {
15
                  "code_travaux" : "3.000.2",
16
                  "quantite" : 1
17
               },
18
               {
19
                  "code_travaux" : "3.000.3",
20
                  "quantite" : 2
21
               }
22
            ]
23
         },
24
         {
25
            "ident" : "ALBE010A",
26
            "libelle_objetgeo" : "Luminaire",
27
            "travaux" : [
28
               {
29
                  "code_travaux" : "3.000.7",
30
                  "quantite" : 1
31
               }
32
            ]
33
         }
34
      ],
35
      "heure_valid" : "10:10",
36
      "libelle_intervention" : "DEPANNAGE EP",
37
      "valid_entreprise" : true,
38
      "valid_sydev" : true
39
   },
40
   {
41
      "code_inter" : "DP.20.007.2",
42
      "date_emission" : "2020-02-01",
43
      "date_valid" : "2020-02-02",
44
      "elements" : [],
45
      "heure_valid" : "17:10",
46
      "libelle_intervention" : "DEPANNAGE EP"
47
   }
48
]
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': '123',  # intervention 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

  
63

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

  
67

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

  
86

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

  
113

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

  
134

  
135
@mock.patch('passerelle.utils.Request.post')
136
def test_query(mocked_post, app, connector):
137
    mocked_post.return_value = QUERY_1
138
    endpoint = get_endpoint('query')
139
    payload = {
140
        'id_intervention': '25523',
141
    }
142
    resp = app.post_json(endpoint, params=payload)
143
    assert mocked_post.mock_calls[0] == mock.call(
144
        'https://dummy-server.org/getIntervention.php/',
145
        headers={'Accept': 'application/json'},
146
        json={'id_intervention': 25523})
147
    assert not resp.json['err']
148
    assert [x['id'] for x in resp.json['data']] == ['DP.20.003.5', 'DP.20.007.2']
149
    assert [x['text'] for x in resp.json['data']] == [
150
        'DP.20.003.5: DEPANNAGE EP', 'DP.20.007.2: DEPANNAGE EP']
151
    payload = {
152
        'date_debut_demande': '2019-01-01',
153
        'date_fin_demande': '2019-01-31',
154
        'insee': '069283:069293',
155
    }
156
    resp = app.post_json(endpoint, params=payload)
157
    assert mocked_post.mock_calls[1] == mock.call(
158
        'https://dummy-server.org/getIntervention.php/',
159
        headers={'Accept': 'application/json'},
160
        json={
161
            'date_debut_demande': '2019-01-01',
162
            'date_fin_demande': '2019-01-31',
163
            'insee': ['069283', '069293'],
164
        })
0
-