0002-astregs-add-initial-connector-33424.patch
passerelle/apps/astregs/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.20 on 2019-06-19 10:24 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.db import migrations, models |
|
6 |
import django.db.models.deletion |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
initial = True |
|
12 | ||
13 |
dependencies = [ |
|
14 |
('base', '0012_job'), |
|
15 |
] |
|
16 | ||
17 |
operations = [ |
|
18 |
migrations.CreateModel( |
|
19 |
name='AstreGS', |
|
20 |
fields=[ |
|
21 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
22 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
23 |
('description', models.TextField(verbose_name='Description')), |
|
24 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
25 |
('wsdl_base_url', models.URLField(verbose_name='Webservices base URL')), |
|
26 |
('username', models.CharField(max_length=32, verbose_name='Username')), |
|
27 |
('password', models.CharField(max_length=32, verbose_name='Password')), |
|
28 |
('organism', models.CharField(max_length=32, verbose_name='Organism')), |
|
29 |
('budget', models.CharField(max_length=32, verbose_name='Budget')), |
|
30 |
('exercice', models.CharField(max_length=32, verbose_name='Exercice')), |
|
31 |
('users', models.ManyToManyField(blank=True, related_name='_astregs_users_+', related_query_name='+', to='base.ApiUser')), |
|
32 |
], |
|
33 |
options={ |
|
34 |
'verbose_name': 'AstresGS', |
|
35 |
}, |
|
36 |
), |
|
37 |
migrations.CreateModel( |
|
38 |
name='Link', |
|
39 |
fields=[ |
|
40 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
41 |
('name_id', models.CharField(max_length=32, verbose_name='User NameID')), |
|
42 |
('association_id', models.CharField(max_length=32, verbose_name='Association ID')), |
|
43 |
('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), |
|
44 |
('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='astregs.AstreGS')), |
|
45 |
], |
|
46 |
), |
|
47 |
migrations.CreateModel( |
|
48 |
name='LinkRequest', |
|
49 |
fields=[ |
|
50 |
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
|
51 |
('token', models.CharField(max_length=16, verbose_name='Temporary token')), |
|
52 |
('association_id', models.CharField(max_length=32, verbose_name='Association ID')), |
|
53 |
('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='astregs.AstreGS')), |
|
54 |
], |
|
55 |
), |
|
56 |
migrations.AlterUniqueTogether( |
|
57 |
name='linkrequest', |
|
58 |
unique_together=set([('resource', 'token', 'association_id')]), |
|
59 |
), |
|
60 |
migrations.AlterUniqueTogether( |
|
61 |
name='link', |
|
62 |
unique_together=set([('resource', 'name_id', 'association_id')]), |
|
63 |
), |
|
64 |
] |
passerelle/apps/astregs/models.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Copyright (C) 2019 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 inspect |
|
18 | ||
19 |
from django.db import models |
|
20 |
from django.utils.translation import ugettext_lazy as _ |
|
21 |
from django.utils.crypto import get_random_string |
|
22 |
from django.utils.text import slugify |
|
23 |
from django.utils.six.moves.urllib import parse as urlparse |
|
24 |
from django.http import Http404 |
|
25 | ||
26 |
from passerelle.base.models import BaseResource |
|
27 |
from passerelle.utils.api import endpoint |
|
28 |
from passerelle.utils.jsonresponse import APIError |
|
29 | ||
30 |
LINK_CREATION_SCHEMA = { |
|
31 |
'$schema': 'http://json-schema.org/draft-03/schema#', |
|
32 |
'title': 'AstreGS Link', |
|
33 |
'description': '', |
|
34 |
'type': 'object', |
|
35 |
'properties': { |
|
36 |
'NameID': { |
|
37 |
'description': 'user name_id', |
|
38 |
'type': 'string', |
|
39 |
'required': True |
|
40 |
}, |
|
41 |
'token': { |
|
42 |
'description': 'linking token', |
|
43 |
'type': 'string', |
|
44 |
'required': True |
|
45 |
} |
|
46 |
} |
|
47 |
} |
|
48 | ||
49 | ||
50 |
class AstreGS(BaseResource): |
|
51 |
wsdl_base_url = models.URLField(_('Webservices base URL')) |
|
52 |
username = models.CharField(_('Username'), max_length=32) |
|
53 |
password = models.CharField(_('Password'), max_length=32) |
|
54 |
organism = models.CharField(_('Organism'), max_length=32) |
|
55 |
budget = models.CharField(_('Budget'), max_length=32) |
|
56 |
exercice = models.CharField(_('Exercice'), max_length=32) |
|
57 | ||
58 |
category = _('Business Process Connectors') |
|
59 | ||
60 |
class Meta: |
|
61 |
verbose_name = u'AstresGS' |
|
62 | ||
63 |
@classmethod |
|
64 |
def get_verbose_name(cls): |
|
65 |
return cls._meta.verbose_name |
|
66 | ||
67 |
def check_status(self): |
|
68 |
response = self.requests.get(self.wsdl_base_url) |
|
69 |
response.raise_for_status() |
|
70 | ||
71 |
@property |
|
72 |
def authentication(self): |
|
73 |
return {'USERNOM': self.username, 'USERPWD': self.password} |
|
74 | ||
75 |
@property |
|
76 |
def context(self): |
|
77 |
return {'Organisme': self.organism, |
|
78 |
'Budget': self.budget, |
|
79 |
'Exercice': self.exercice} |
|
80 | ||
81 |
def get_client(self, wsdl_name): |
|
82 |
url = urlparse.urljoin(self.wsdl_base_url, '%s?wsdl' % wsdl_name) |
|
83 |
return self.soap_client(wsdl_url=url) |
|
84 | ||
85 |
@endpoint(description=_('Find associations by SIREN number'), |
|
86 |
perm='can_access', |
|
87 |
parameters={ |
|
88 |
'siren': {'description': _('SIREN Number'), |
|
89 |
'example_value': '77567227216096'} |
|
90 |
} |
|
91 |
) |
|
92 |
def associations(self, request, siren): |
|
93 |
client = self.get_client('RechercheTiersDetails') |
|
94 |
r = client.service.liste(Authentification=self.authentication, |
|
95 |
Contexte=self.context, |
|
96 |
Criteres={'siren': '%s*' % siren}) |
|
97 |
data = [] |
|
98 |
if r.liste: |
|
99 |
for item in r.liste.EnregRechercheTiersDetailsReturn: |
|
100 |
association_data = dict(inspect.getmembers(item)) |
|
101 |
association_data['id'] = item.Numero_SIRET |
|
102 |
association_data['text'] = '%s - %s' % (item.Numero_SIRET, item.Nom_enregistrement) |
|
103 |
association_data['code'] = item.Code_tiers |
|
104 |
association_data['name'] = item.Nom_enregistrement |
|
105 |
association_data['address'] = item.Nom_enregistrement |
|
106 |
data.append(association_data) |
|
107 |
return {'data': data} |
|
108 | ||
109 |
@endpoint(description=_('Check if association exists by its SIRET number'), |
|
110 |
name='verify-association-presence-by-siret', |
|
111 |
perm='can_access', |
|
112 |
parameters={ |
|
113 |
'siret': {'description': _('SIRET Number'), |
|
114 |
'example_value': '7756722721609600014'} |
|
115 |
} |
|
116 |
) |
|
117 |
def verify_association_presence(self, request, siret): |
|
118 |
client = self.get_client('RechercheTiers') |
|
119 |
r = client.service.liste(Authentification=self.authentication, |
|
120 |
Contexte=self.context, |
|
121 |
Criteres={'siren': siret}) |
|
122 |
if r.liste: |
|
123 |
return {'exists': True} |
|
124 |
return {'exists': False} |
|
125 | ||
126 |
@endpoint(name='get-association-link-means', |
|
127 |
description=_('Get association linking means'), |
|
128 |
perm='can_access', |
|
129 |
parameters={ |
|
130 |
'association_id': {'description': _('Association ID'), |
|
131 |
'example_value': '42435'} |
|
132 |
} |
|
133 |
) |
|
134 |
def get_association_link_means(self, request, association_id): |
|
135 |
client = self.get_client('Tiers') |
|
136 |
r = client.service.Chargement({'Authentification': self.authentication, |
|
137 |
'Contexte': self.context, |
|
138 |
'TiersCle': {'CodeTiers': association_id}}) |
|
139 |
data = [] |
|
140 |
# assocation contact is defined in EncodeKeyContact attribute |
|
141 |
if not r.EncodeKeyContact: |
|
142 |
return {'data': data} |
|
143 | ||
144 |
client = self.get_client('Contact') |
|
145 |
r = client.service.Chargement({'Authentification': self.authentication, |
|
146 |
'Contexte': self.context, |
|
147 |
'ContactCle': {'idContact': r.EncodeKeyContact}}) |
|
148 |
if r.AdresseMail or r.TelephoneMobile or r.RueVoie: |
|
149 |
try: |
|
150 |
link_request = LinkRequest.objects.get(resource=self, association_id=association_id) |
|
151 |
except LinkRequest.DoesNotExist: |
|
152 |
token = get_random_string() |
|
153 |
link_request = LinkRequest.objects.create(resource=self, association_id=association_id, |
|
154 |
token=token) |
|
155 |
if r.AdresseMail: |
|
156 |
data.append({'id': '1', |
|
157 |
'text': r.AdresseMail, |
|
158 |
'value': r.AdresseMail, |
|
159 |
'token': link_request.token, |
|
160 |
'type': 'email'}) |
|
161 |
if r.TelephoneMobile: |
|
162 |
data.append({'id': '2', |
|
163 |
'text': r.TelephoneMobile, |
|
164 |
'value': r.TelephoneMobile, |
|
165 |
'token': link_request.token, |
|
166 |
'type': 'mobile'}) |
|
167 |
if r.RueVoie: |
|
168 |
full_address = r.RueVoie |
|
169 |
full_address += ', ' |
|
170 |
if r.CodePostal: |
|
171 |
full_address += '%s ' % r.CodePostal |
|
172 |
if r.Ville: |
|
173 |
full_address += r.Ville |
|
174 |
data.append({'id': '3', |
|
175 |
'text': full_address, |
|
176 |
'address': r.RueVoie, |
|
177 |
'zicode': r.CodePostal, |
|
178 |
'city': r.Ville, |
|
179 |
'value': full_address, |
|
180 |
'token': link_request.token, |
|
181 |
'type': 'mail'}) |
|
182 |
return {'data': data} |
|
183 | ||
184 |
@endpoint(name='link', perm='can_access', |
|
185 |
post={ |
|
186 |
'description': _('Create link with an association'), |
|
187 |
'request_body': { |
|
188 |
'schema': { |
|
189 |
'application/json': LINK_CREATION_SCHEMA |
|
190 |
} |
|
191 |
} |
|
192 |
} |
|
193 |
) |
|
194 |
def link(self, request, post_data): |
|
195 |
try: |
|
196 |
link_request = LinkRequest.objects.get(resource=self, token=post_data['token']) |
|
197 |
# if link request found, create link |
|
198 |
link = Link.objects.create(resource=self, name_id=post_data['NameID'], association_id=link_request.association_id) |
|
199 |
# then delete link request |
|
200 |
link_request.delete() |
|
201 |
return {'link': link.id, 'created': link.created, 'association_id': link_request.association_id} |
|
202 |
except LinkRequest.DoesNotExist: |
|
203 |
raise Http404('token not found') |
|
204 | ||
205 | ||
206 |
@endpoint(description=_('Remove link to an association'), |
|
207 |
perm='can_access', |
|
208 |
parameters={ |
|
209 |
'NameID':{ |
|
210 |
'description': _('Publik NameID'), |
|
211 |
'example_value': 'xyz24d934', |
|
212 |
}, |
|
213 |
'association_id':{ |
|
214 |
'description': _('Association ID'), |
|
215 |
'example_value': '12345', |
|
216 |
} |
|
217 |
}) |
|
218 |
def unlink(self, request, NameID, association_id): |
|
219 |
try: |
|
220 |
link = Link.objects.get(resource=self, name_id=NameID, association_id=association_id) |
|
221 |
link.delete() |
|
222 |
return {'deleted': True} |
|
223 |
except Link.DoesNotExist: |
|
224 |
raise Http404('link not found') |
|
225 | ||
226 |
@endpoint(description=_('List user links to associations'), |
|
227 |
perm='can_access', |
|
228 |
parameters={ |
|
229 |
'NameID':{ |
|
230 |
'description': _('User NameID'), |
|
231 |
'example_value': 'xyz24d934', |
|
232 |
} |
|
233 |
}) |
|
234 |
def links(self, request, NameID): |
|
235 |
data = [] |
|
236 |
for link in Link.objects.filter(resource=self, name_id=NameID): |
|
237 |
data.append({'association_id': link.association_id, |
|
238 |
'created': link.created}) |
|
239 |
return {'data': data} |
|
240 | ||
241 | ||
242 |
class Link(models.Model): |
|
243 |
resource = models.ForeignKey(AstreGS) |
|
244 |
name_id = models.CharField(_('User NameID'), max_length=32) |
|
245 |
association_id = models.CharField(_('Association ID'), max_length=32) |
|
246 |
created = models.DateTimeField(_('Creation date'), auto_now_add=True) |
|
247 | ||
248 |
class Meta: |
|
249 |
unique_together = ('resource', 'name_id', 'association_id') |
|
250 | ||
251 | ||
252 |
class LinkRequest(models.Model): |
|
253 |
resource = models.ForeignKey(AstreGS) |
|
254 |
token = models.CharField(_('Temporary token'), max_length=16) |
|
255 |
association_id = models.CharField(_('Association ID'), max_length=32) |
|
256 | ||
257 |
class Meta: |
|
258 |
unique_together = ('resource', 'token', 'association_id') |
passerelle/settings.py | ||
---|---|---|
123 | 123 |
'passerelle.apps.api_particulier', |
124 | 124 |
'passerelle.apps.arcgis', |
125 | 125 |
'passerelle.apps.arpege_ecp', |
126 |
'passerelle.apps.astregs', |
|
126 | 127 |
'passerelle.apps.atal', |
127 | 128 |
'passerelle.apps.atos_genesys', |
128 | 129 |
'passerelle.apps.base_adresse', |
tests/test_astregs.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 | ||
3 |
import mock |
|
4 |
import pytest |
|
5 | ||
6 |
from passerelle.apps.astregs.models import AstreGS, LinkRequest, Link |
|
7 | ||
8 |
from . import utils |
|
9 | ||
10 |
BASE_URL = 'https://test-ws-astre-gs.departement06.fr/axis2/services/' |
|
11 | ||
12 | ||
13 |
class FakeSearchDetailsService(): |
|
14 |
def liste(self, Authentification, Contexte, Criteres): |
|
15 |
class FakeObject(): |
|
16 |
Numero_SIRET='50043390900016' |
|
17 |
Nom_enregistrement='ASSOCIATION OMNISPORTS DES MONTS D AZUR' |
|
18 |
Code_tiers='173957' |
|
19 |
liste = mock.Mock(EnregRechercheTiersDetailsReturn=[FakeObject()]) |
|
20 |
return mock.Mock(liste=liste) |
|
21 | ||
22 | ||
23 |
class FakeSearchService(): |
|
24 |
def liste(self, Authentification, Contexte, Criteres): |
|
25 |
if Criteres['siren'] == 'unknown': |
|
26 |
return mock.Mock(liste=False) |
|
27 |
return mock.Mock(liste=True) |
|
28 | ||
29 | ||
30 |
class FakeTiersService(): |
|
31 |
def Chargement(self, data): |
|
32 |
if data['TiersCle']['CodeTiers'] == '42': |
|
33 |
return mock.Mock(EncodeKeyContact='4242') |
|
34 |
return mock.Mock(EncodeKeyContact=None) |
|
35 | ||
36 | ||
37 |
class FakeContactService(): |
|
38 |
def Chargement(self, data): |
|
39 |
return mock.Mock(AdresseMail='foo@example.com', |
|
40 |
TelephoneMobile='0607080900', |
|
41 |
RueVoie='169 rue du Château', |
|
42 |
CodePostal='75014', |
|
43 |
Ville='Paris') |
|
44 | ||
45 | ||
46 |
def search_side_effect(wsdl_url, **kwargs): |
|
47 |
if 'RechercheTiersDetail' in wsdl_url: |
|
48 |
return mock.Mock(service=FakeSearchDetailsService()) |
|
49 |
return mock.Mock(service=FakeSearchService()) |
|
50 | ||
51 |
def contact_search_side_effect(wsdl_url, **kwargs): |
|
52 |
if 'Tiers' in wsdl_url: |
|
53 |
return mock.Mock(service=FakeTiersService()) |
|
54 |
return mock.Mock(service=FakeContactService()) |
|
55 | ||
56 | ||
57 |
@pytest.fixture |
|
58 |
def connector(db): |
|
59 |
return utils.make_resource(AstreGS, |
|
60 |
title='Test', slug='test', |
|
61 |
description='test', wsdl_base_url=BASE_URL, |
|
62 |
username='CS-FORML', password='secret', |
|
63 |
organism='CG06', budget='01', |
|
64 |
exercice='2019' |
|
65 |
) |
|
66 | ||
67 | ||
68 |
@mock.patch('passerelle.base.models.BaseResource.soap_client', side_effect=search_side_effect) |
|
69 |
def test_search_association_by_siren(client, connector, app): |
|
70 |
resp = app.get('/astregs/test/associations', params={'siren': '500433909'}) |
|
71 |
assert isinstance(resp.json['data'], list) |
|
72 |
assert len(resp.json['data']) > 0 |
|
73 |
assert resp.json['data'][0]['id'] == '50043390900016' |
|
74 |
assert resp.json['data'][0]['text'] == '50043390900016 - ASSOCIATION OMNISPORTS DES MONTS D AZUR' |
|
75 |
assert resp.json['data'][0]['code'] == '173957' |
|
76 |
assert resp.json['data'][0]['name'] == 'ASSOCIATION OMNISPORTS DES MONTS D AZUR' |
|
77 |
assert client.call_args[1]['wsdl_url'] == '%sRechercheTiersDetails?wsdl' % BASE_URL |
|
78 | ||
79 | ||
80 |
@mock.patch('passerelle.base.models.BaseResource.soap_client', side_effect=search_side_effect) |
|
81 |
def test_check_association_presence(client, connector, app): |
|
82 |
resp = app.get('/astregs/test/verify-association-presence-by-siret', params={'siret': '50043390900014'}) |
|
83 |
assert resp.json['exists'] == True |
|
84 |
assert client.call_args[1]['wsdl_url'] == '%sRechercheTiers?wsdl' % BASE_URL |
|
85 |
resp = app.get('/astregs/test/verify-association-presence-by-siret', params={'siret': 'unknown'}) |
|
86 |
assert resp.json['exists'] == False |
|
87 | ||
88 |
@mock.patch('passerelle.base.models.BaseResource.soap_client', side_effect=contact_search_side_effect) |
|
89 |
def test_association_linking_means(client, connector, app): |
|
90 |
resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '000'}) |
|
91 |
assert resp.json['data'] == [] |
|
92 |
assert LinkRequest.objects.count() == 0 |
|
93 | ||
94 |
resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'}) |
|
95 |
assert len(resp.json['data']) == 3 |
|
96 |
assert LinkRequest.objects.filter(association_id='42').count() == 1 |
|
97 |
for result in resp.json['data']: |
|
98 |
assert 'id' in result |
|
99 |
assert 'text' in result |
|
100 |
assert 'type' in result |
|
101 |
assert 'token' in result |
|
102 | ||
103 | ||
104 |
@mock.patch('passerelle.base.models.BaseResource.soap_client', side_effect=contact_search_side_effect) |
|
105 |
def test_link_user_to_association(client, connector, app): |
|
106 |
assert LinkRequest.objects.count() == 0 |
|
107 |
resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'}) |
|
108 |
assert len(resp.json['data']) == 3 |
|
109 |
token = resp.json['data'][0]['token'] |
|
110 |
assert LinkRequest.objects.filter(association_id='42').count() == 1 |
|
111 |
resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'}) |
|
112 |
assert LinkRequest.objects.filter(association_id='42').count() == 0 |
|
113 |
assert Link.objects.filter(name_id='user_name_id', association_id='42').count() == 1 |
|
114 |
link = Link.objects.get(name_id='user_name_id', association_id='42') |
|
115 |
assert resp.json['association_id'] == link.association_id |
|
116 |
assert resp.json['link'] == link.pk |
|
117 |
assert 'created' in resp.json |
|
118 |
# try to link again |
|
119 |
resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'}, status=404) |
|
120 | ||
121 | ||
122 |
@mock.patch('passerelle.base.models.BaseResource.soap_client', side_effect=contact_search_side_effect) |
|
123 |
def test_list_user_links(client, connector, app): |
|
124 |
resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'}) |
|
125 |
token = resp.json['data'][0]['token'] |
|
126 |
resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'}) |
|
127 |
resp = app.get('/astregs/test/links', params={'NameID': 'user_name_id'}) |
|
128 |
assert len(resp.json['data']) > 0 |
|
129 |
for link in resp.json['data']: |
|
130 |
assert 'association_id' in link |
|
131 |
assert 'created' in link |
|
132 | ||
133 |
resp = app.get('/astregs/test/links', params={'NameID': 'foo_name_id'}) |
|
134 |
assert len(resp.json['data']) == 0 |
|
135 | ||
136 | ||
137 |
@mock.patch('passerelle.base.models.BaseResource.soap_client', side_effect=contact_search_side_effect) |
|
138 |
def test_unlink_user_from_association(client, connector, app): |
|
139 |
resp = app.get('/astregs/test/get-association-link-means', params={'association_id': '42'}) |
|
140 |
token = resp.json['data'][0]['token'] |
|
141 |
resp = app.post_json('/astregs/test/link', params={'token': token, 'NameID': 'user_name_id'}) |
|
142 |
resp = app.get('/astregs/test/unlink', params={'NameID': 'user_name_id', 'association_id': '42'}) |
|
143 |
assert resp.json['deleted'] |
|
144 |
resp = app.get('/astregs/test/unlink', params={'NameID': 'user_name_id', 'association_id': '42'}, status=404) |
|
0 |
- |