0001-add-Isere-ENS-connector-50019.patch
passerelle/contrib/isere_ens/migrations/0001_initial.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.18 on 2021-01-19 13:09 |
|
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', '0028_rename_permissions'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.CreateModel( |
|
18 |
name='IsereENS', |
|
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.URLField(help_text='Base API URL (before /api/...)', verbose_name='Webservice Base URL')), |
|
31 |
('token', models.CharField(max_length=128, verbose_name='Access token')), |
|
32 |
('users', models.ManyToManyField(blank=True, related_name='_isereens_users_+', related_query_name='+', to='base.ApiUser')), |
|
33 |
], |
|
34 |
options={ |
|
35 |
'verbose_name': 'Espaces naturels sensibles du CD38', |
|
36 |
}, |
|
37 |
), |
|
38 |
] |
passerelle/contrib/isere_ens/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 collections import OrderedDict |
|
18 |
import datetime |
|
19 | ||
20 |
from django.core.cache import cache |
|
21 |
from django.db import models |
|
22 |
from django.utils.formats import date_format |
|
23 |
from django.utils.six.moves.urllib import parse as urlparse |
|
24 |
from django.utils.translation import ugettext_lazy as _ |
|
25 | ||
26 |
from passerelle.utils.conversion import simplify |
|
27 |
from passerelle.utils.jsonresponse import APIError |
|
28 |
from passerelle.utils.api import endpoint |
|
29 |
from passerelle.base.models import BaseResource, HTTPResource |
|
30 | ||
31 | ||
32 |
SITE_BOOKING_SCHOOL_SCHEMA = { |
|
33 |
"$schema": "http://json-schema.org/draft-04/schema#", |
|
34 |
"title": "ENS site/booking/school", |
|
35 |
"description": "", |
|
36 |
"type": "object", |
|
37 |
"required": [ |
|
38 |
"site", |
|
39 |
"project", |
|
40 |
"date", |
|
41 |
"pmr", |
|
42 |
"morning", |
|
43 |
"lunch", |
|
44 |
"afternoon", |
|
45 |
"participants", |
|
46 |
"animator", |
|
47 |
"school", |
|
48 |
"grade_levels", |
|
49 |
"beneficiary_first_name", |
|
50 |
"beneficiary_last_name", |
|
51 |
"beneficiary_email", |
|
52 |
"beneficiary_phone", |
|
53 |
"beneficiary_cellphone", |
|
54 |
], |
|
55 |
"properties": OrderedDict( |
|
56 |
{ |
|
57 |
"site": { |
|
58 |
"description": "site id (code)", |
|
59 |
"type": "string", |
|
60 |
}, |
|
61 |
"project": { |
|
62 |
"description": "project code", |
|
63 |
"type": "string", |
|
64 |
}, |
|
65 |
"date": { |
|
66 |
"description": "booking date (format: YYYY-MM-DD)", |
|
67 |
"type": "string", |
|
68 |
}, |
|
69 |
"pmr": { |
|
70 |
"description": "PMR", |
|
71 |
"type": "boolean", |
|
72 |
}, |
|
73 |
"morning": { |
|
74 |
"description": "morning booking", |
|
75 |
"type": "boolean", |
|
76 |
}, |
|
77 |
"lunch": { |
|
78 |
"description": "lunch booking", |
|
79 |
"type": "boolean", |
|
80 |
}, |
|
81 |
"afternoon": { |
|
82 |
"description": "afternoon booking", |
|
83 |
"type": "boolean", |
|
84 |
}, |
|
85 |
"participants": { |
|
86 |
"description": "number of participants", |
|
87 |
"type": "string", |
|
88 |
"pattern": "[0-9]+", |
|
89 |
}, |
|
90 |
"animator": { |
|
91 |
"description": "animator id", |
|
92 |
"type": "string", |
|
93 |
}, |
|
94 |
"school": { |
|
95 |
"description": "school UAI (RNE) code", |
|
96 |
"type": "string", |
|
97 |
}, |
|
98 |
"grade_levels": { |
|
99 |
"description": "grade levels", |
|
100 |
"type": "array", |
|
101 |
"items": { |
|
102 |
"type": "string", |
|
103 |
"description": "level", |
|
104 |
}, |
|
105 |
}, |
|
106 |
"beneficiary_first_name": { |
|
107 |
"description": "beneficiary first name", |
|
108 |
"type": "string", |
|
109 |
}, |
|
110 |
"beneficiary_last_name": { |
|
111 |
"description": "beneficiary last name", |
|
112 |
"type": "string", |
|
113 |
}, |
|
114 |
"beneficiary_email": { |
|
115 |
"description": "beneficiary email", |
|
116 |
"type": "string", |
|
117 |
}, |
|
118 |
"beneficiary_phone": { |
|
119 |
"description": "beneficiary phone number", |
|
120 |
"type": "string", |
|
121 |
}, |
|
122 |
"beneficiary_cellphone": { |
|
123 |
"description": "beneficiary cell phone number", |
|
124 |
"type": "string", |
|
125 |
}, |
|
126 |
} |
|
127 |
), |
|
128 |
} |
|
129 | ||
130 | ||
131 |
class IsereENS(BaseResource, HTTPResource): |
|
132 |
category = _("Business Process Connectors") |
|
133 | ||
134 |
base_url = models.URLField( |
|
135 |
verbose_name=_("Webservice Base URL"), |
|
136 |
help_text=_("Base API URL (before /api/...)"), |
|
137 |
) |
|
138 |
token = models.CharField(verbose_name=_("Access token"), max_length=128) |
|
139 | ||
140 |
class Meta: |
|
141 |
verbose_name = _("Espaces naturels sensibles de l'Isère") |
|
142 | ||
143 |
def request(self, endpoint, params=None, json=None): |
|
144 |
url = urlparse.urljoin(self.base_url, endpoint) |
|
145 |
headers = {"token": self.token} |
|
146 |
if json is not None: |
|
147 |
response = self.requests.post( |
|
148 |
url, params=params, json=json, headers=headers |
|
149 |
) |
|
150 |
else: |
|
151 |
response = self.requests.get(url, params=params, headers=headers) |
|
152 | ||
153 |
if response.status_code // 100 != 2: |
|
154 |
try: |
|
155 |
json_content = response.json() |
|
156 |
except ValueError: |
|
157 |
json_content = None |
|
158 |
raise APIError( |
|
159 |
"error status:%s %r, content:%r" |
|
160 |
% (response.status_code, response.reason, response.content[:1024]), |
|
161 |
data={ |
|
162 |
"status_code": response.status_code, |
|
163 |
"json_content": json_content, |
|
164 |
}, |
|
165 |
) |
|
166 | ||
167 |
if response.status_code == 204: # 204 No Content |
|
168 |
raise APIError('abnormal empty response') |
|
169 | ||
170 |
try: |
|
171 |
return response.json() |
|
172 |
except ValueError: |
|
173 |
raise APIError("invalid JSON in response: %r" % response.content[:1024]) |
|
174 | ||
175 |
@endpoint( |
|
176 |
name="sites", |
|
177 |
description=_("Sites"), |
|
178 |
display_order=1, |
|
179 |
perm="can_access", |
|
180 |
parameters={ |
|
181 |
"q": {"description": _("Search text in name field")}, |
|
182 |
"id": { |
|
183 |
"description": _("Returns site with code=id"), |
|
184 |
}, |
|
185 |
"kind": { |
|
186 |
"description": _("Select sites by bind: school_group or social"), |
|
187 |
}, |
|
188 |
}, |
|
189 |
) |
|
190 |
def sites(self, request, q=None, id=None, kind=None): |
|
191 |
if id is not None: |
|
192 |
site = self.request("api/2.0.0/site/" + id) |
|
193 |
site["id"] = site["code"] |
|
194 |
site["text"] = "%(name)s (%(city)s)" % site |
|
195 |
sites = [site] |
|
196 |
else: |
|
197 |
cache_key = "isere-ens-sites-%d" % self.id |
|
198 |
sites = cache.get(cache_key) |
|
199 |
if not sites: |
|
200 |
sites = self.request("api/2.0.0/site") |
|
201 |
for site in sites: |
|
202 |
site["id"] = site["code"] |
|
203 |
site["text"] = "%(name)s (%(city)s)" % site |
|
204 |
cache.set(cache_key, sites, 300) |
|
205 |
if kind is not None: |
|
206 |
sites = [site for site in sites if site.get(kind)] |
|
207 |
if q is not None: |
|
208 |
q = simplify(q) |
|
209 |
sites = [site for site in sites if q in simplify(site["text"])] |
|
210 |
return {"data": sites} |
|
211 | ||
212 |
@endpoint( |
|
213 |
name="animators", |
|
214 |
description=_("Animators"), |
|
215 |
display_order=2, |
|
216 |
perm="can_access", |
|
217 |
parameters={ |
|
218 |
"q": {"description": _("Search text in name field")}, |
|
219 |
"id": { |
|
220 |
"description": _("Returns animator number id"), |
|
221 |
}, |
|
222 |
}, |
|
223 |
) |
|
224 |
def animators(self, request, q=None, id=None): |
|
225 |
cache_key = "isere-ens-animators-%d" % self.id |
|
226 |
animators = cache.get(cache_key) |
|
227 |
if not animators: |
|
228 |
animators = self.request("api/2.0.0/schoolAnimator") |
|
229 |
for animator in animators: |
|
230 |
animator["id"] = str(animator["id"]) |
|
231 |
animator["text"] = ( |
|
232 |
"%(first_name)s %(last_name)s <%(email)s> (%(entity)s)" % animator |
|
233 |
) |
|
234 |
cache.set(cache_key, animators, 300) |
|
235 |
if id is not None: |
|
236 |
animators = [animator for animator in animators if animator["id"] == id] |
|
237 |
if q is not None: |
|
238 |
q = simplify(q) |
|
239 |
animators = [ |
|
240 |
animator for animator in animators if q in simplify(animator["text"]) |
|
241 |
] |
|
242 |
return {"data": animators} |
|
243 | ||
244 |
@endpoint( |
|
245 |
name="site-calendar", |
|
246 |
description=_("Available bookings for a site"), |
|
247 |
display_order=3, |
|
248 |
perm="can_access", |
|
249 |
parameters={ |
|
250 |
"site": {"description": _("Site code (aka id)")}, |
|
251 |
"participants": { |
|
252 |
"description": _("Number of participants"), |
|
253 |
}, |
|
254 |
"start_date": { |
|
255 |
"description": _( |
|
256 |
"First date of the calendar (format: YYYY-MM-DD, default: today)" |
|
257 |
), |
|
258 |
}, |
|
259 |
"end_date": { |
|
260 |
"description": _( |
|
261 |
"Last date of the calendar (format: YYYY-MM-DD, default: start_date + 92 days)" |
|
262 |
), |
|
263 |
}, |
|
264 |
}, |
|
265 |
) |
|
266 |
def site_calendar( |
|
267 |
self, request, site, participants="1", start_date=None, end_date=None |
|
268 |
): |
|
269 |
if start_date: |
|
270 |
try: |
|
271 |
start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date() |
|
272 |
except ValueError: |
|
273 |
raise APIError( |
|
274 |
"bad start_date format (%s), should be YYYY-MM-DD" % start_date, |
|
275 |
http_status=400, |
|
276 |
) |
|
277 |
else: |
|
278 |
start_date = datetime.date.today() |
|
279 |
if end_date: |
|
280 |
try: |
|
281 |
end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date() |
|
282 |
except ValueError: |
|
283 |
raise APIError( |
|
284 |
"bad end_date format (%s), should be YYYY-MM-DD" % end_date, |
|
285 |
http_status=400, |
|
286 |
) |
|
287 |
else: |
|
288 |
end_date = start_date + datetime.timedelta(days=92) |
|
289 | ||
290 |
params = { |
|
291 |
"site": site, |
|
292 |
"participants": participants, |
|
293 |
"start_date": start_date.strftime("%Y-%m-%d"), |
|
294 |
"end_date": end_date.strftime("%Y-%m-%d"), |
|
295 |
} |
|
296 |
dates = self.request("api/2.0.0/site/" + site + "/calendar", params=params) |
|
297 | ||
298 |
for date in dates: |
|
299 |
date["id"] = site + ":" + date["date"] |
|
300 |
date["site"] = site |
|
301 |
date_ = datetime.datetime.strptime(date["date"], "%Y-%m-%d").date() |
|
302 |
date["date_format"] = date_format(date_, format="DATE_FORMAT") |
|
303 |
date["disabled"] = False |
|
304 |
date["color"] = "green" |
|
305 |
for period in ("morning", "afternoon"): |
|
306 |
if date[period] == "COMPLETE": |
|
307 |
date["color"] = "orange" |
|
308 |
date["%s_status" % period] = _("Complete") |
|
309 |
else: |
|
310 |
date["%s_status" % period] = _("Available") |
|
311 |
if date["morning"] == "COMPLETE" and date["afternoon"] == "COMPLETE": |
|
312 |
date["disabled"] = True |
|
313 |
date["color"] = "red" |
|
314 |
if date["lunch"] == "CLOSE": |
|
315 |
date["lunch_status"] = _("Complete") |
|
316 |
else: |
|
317 |
date["lunch_status"] = _("Available") |
|
318 |
date["details"] = _( |
|
319 |
"Morning (%(morning_status)s), Lunch (%(lunch_status)s), Afternoon (%(afternoon_status)s)" |
|
320 |
% date |
|
321 |
) |
|
322 |
date["text"] = "%(date_format)s - %(details)s" % date |
|
323 |
return {"data": dates} |
|
324 | ||
325 |
@endpoint( |
|
326 |
name="site-booking-school", |
|
327 |
description=_("Booking a site for a school"), |
|
328 |
display_order=4, |
|
329 |
perm="can_access", |
|
330 |
methods=["post"], |
|
331 |
post={ |
|
332 |
"request_body": { |
|
333 |
"schema": { |
|
334 |
"application/json": SITE_BOOKING_SCHOOL_SCHEMA, |
|
335 |
} |
|
336 |
} |
|
337 |
}, |
|
338 |
) |
|
339 |
def site_booking_school(self, request, post_data): |
|
340 |
payload = { |
|
341 |
"siteCode": post_data["site"], |
|
342 |
"projectCode": post_data["project"], |
|
343 |
"bookingDate": post_data["date"], |
|
344 |
"pmr": post_data["pmr"], |
|
345 |
"morning": post_data["morning"], |
|
346 |
"lunch": post_data["lunch"], |
|
347 |
"afternoon": post_data["afternoon"], |
|
348 |
"participants": int(post_data["participants"]), |
|
349 |
"schoolAnimator": post_data["animator"], |
|
350 |
"school": post_data["school"], |
|
351 |
"gradeLevels": post_data["grade_levels"], |
|
352 |
"beneficiary": { |
|
353 |
"firstName": post_data["beneficiary_first_name"], |
|
354 |
"lastName": post_data["beneficiary_last_name"], |
|
355 |
"email": post_data["beneficiary_email"], |
|
356 |
"phone": post_data["beneficiary_phone"], |
|
357 |
"cellphone": post_data["beneficiary_cellphone"], |
|
358 |
}, |
|
359 |
} |
|
360 |
booking = self.request("api/2.0.0/site/booking/school", json=payload) |
|
361 |
return {"data": booking} |
tests/settings.py | ||
---|---|---|
22 | 22 |
'passerelle.contrib.grandlyon_streetsections', |
23 | 23 |
'passerelle.contrib.greco', |
24 | 24 |
'passerelle.contrib.grenoble_gru', |
25 |
'passerelle.contrib.isere_ens', |
|
25 | 26 |
'passerelle.contrib.iparapheur', |
26 | 27 |
'passerelle.contrib.iws', |
27 | 28 |
'passerelle.contrib.lille_urban_card', |
tests/test_isere_ens.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Passerelle - uniform access to data and services |
|
3 |
# Copyright (C) 2021 Entr'ouvert |
|
4 |
# |
|
5 |
# This program is free software: you can redistribute it and/or modify it |
|
6 |
# under the terms of the GNU Affero General Public License as published |
|
7 |
# by the Free Software Foundation, either version 3 of the License, or |
|
8 |
# (at your option) any later version. |
|
9 |
# |
|
10 |
# This program is distributed in the hope that it will be useful, |
|
11 |
# but WITHOUT ANY WARRANTY; exclude even the implied warranty of |
|
12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 |
# GNU Affero General Public License for more details. |
|
14 |
# |
|
15 |
# You should have received a.deepcopy of the GNU Affero General Public License |
|
16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 | ||
18 | ||
19 |
import json |
|
20 |
import os |
|
21 |
import mock |
|
22 |
import pytest |
|
23 | ||
24 |
import utils |
|
25 | ||
26 |
from django.urls import reverse |
|
27 | ||
28 |
from passerelle.contrib.isere_ens.models import IsereENS |
|
29 |
from passerelle.utils.jsonresponse import APIError |
|
30 | ||
31 | ||
32 |
@pytest.fixture |
|
33 |
def setup(db): |
|
34 |
return utils.setup_access_rights( |
|
35 |
IsereENS.objects.create( |
|
36 |
slug="test", base_url="https://ens38.example.net/", token="toktok" |
|
37 |
) |
|
38 |
) |
|
39 | ||
40 | ||
41 |
SITES_RESPONSE = """[ |
|
42 |
{ |
|
43 |
"name": "Save - étangs de la Serre", |
|
44 |
"code": "SD29a", |
|
45 |
"city": "Arandon-Passins - Courtenay", |
|
46 |
"school_group": true, |
|
47 |
"social": true |
|
48 |
}, |
|
49 |
{ |
|
50 |
"name": "Save - étangs de Passins", |
|
51 |
"code": "SD29b", |
|
52 |
"city": "Arandon-Passins", |
|
53 |
"school_group": true, |
|
54 |
"social": true |
|
55 |
}, |
|
56 |
{ |
|
57 |
"name": "Save - lac de Save", |
|
58 |
"code": "SD29c", |
|
59 |
"city": "Arandon-Passins", |
|
60 |
"school_group": true, |
|
61 |
"social": false |
|
62 |
} |
|
63 |
]""" |
|
64 | ||
65 |
SD29B_RESPONSE = """{ |
|
66 |
"name": "Save - étangs de Passins", |
|
67 |
"code": "SD29b", |
|
68 |
"type": "DEPARTMENTAL_ENS", |
|
69 |
"city": "Arandon-Passins", |
|
70 |
"natural_environment": "Etangs, tourbières, mares, forêts, rivière, pelouses sèches", |
|
71 |
"lunch_equipment": null, |
|
72 |
"pmr_access": "NO", |
|
73 |
"school_group": true, |
|
74 |
"social": true, |
|
75 |
"user_informations": "<p>Un album jeunesse a été réalisé sur l'ENS de la Save. Il permet aux enfants de s'approprier le site en amont d'une sortie.</p>", |
|
76 |
"dogs": "LEASH", |
|
77 |
"educational_equipments": null, |
|
78 |
"eps_regulation": null, |
|
79 |
"main_manager": { |
|
80 |
"first_name": "Jo", |
|
81 |
"last_name": "Smith", |
|
82 |
"managing_entity": "Département de l'Isère", |
|
83 |
"main_phone": "01 23 45 67 89", |
|
84 |
"email": "jo.smith@example.net" |
|
85 |
} |
|
86 |
}""" |
|
87 | ||
88 |
SITE_404_RESPONSE = """{ |
|
89 |
"user_message": "Impossible de trouver le site", |
|
90 |
"message": "Site not found with code SD29x" |
|
91 |
}""" |
|
92 | ||
93 |
ANIMATORS_RESPONSE = """[ |
|
94 |
{ |
|
95 |
"id": 1, |
|
96 |
"first_name": "Francis", |
|
97 |
"last_name": "Kuntz", |
|
98 |
"email": "fk@mail.grd", |
|
99 |
"phone": "123", |
|
100 |
"entity": "Association Nature Morte" |
|
101 |
}, |
|
102 |
{ |
|
103 |
"id": 2, |
|
104 |
"first_name": "Michael", |
|
105 |
"last_name": "Kael", |
|
106 |
"email": "mk@mail.grd", |
|
107 |
"phone": "456", |
|
108 |
"entity": "Association Porte de l'Enfer" |
|
109 |
} |
|
110 |
]""" |
|
111 | ||
112 |
SITE_CALENDAR_RESPONSE = """[ |
|
113 |
{ |
|
114 |
"date": "2020-01-21", |
|
115 |
"morning": "AVAILABLE", |
|
116 |
"lunch": "CLOSE", |
|
117 |
"afternoon": "AVAILABLE" |
|
118 |
}, |
|
119 |
{ |
|
120 |
"date": "2020-01-22", |
|
121 |
"morning": "AVAILABLE", |
|
122 |
"lunch": "OPEN", |
|
123 |
"afternoon": "COMPLETE" |
|
124 |
}, |
|
125 |
{ |
|
126 |
"date": "2020-01-23", |
|
127 |
"morning": "COMPLETE", |
|
128 |
"lunch": "CLOSE", |
|
129 |
"afternoon": "COMPLETE" |
|
130 |
} |
|
131 |
]""" |
|
132 | ||
133 |
BOOK_RESPONSE = """{ |
|
134 |
"status": "OK" |
|
135 |
}""" |
|
136 | ||
137 | ||
138 |
@mock.patch("passerelle.utils.Request.get") |
|
139 |
def test_get_sites(mocked_get, app, setup): |
|
140 |
mocked_get.return_value = utils.FakedResponse( |
|
141 |
content=SITES_RESPONSE, status_code=200 |
|
142 |
) |
|
143 |
endpoint = reverse( |
|
144 |
"generic-endpoint", |
|
145 |
kwargs={"connector": "isere-ens", "slug": setup.slug, "endpoint": "sites"}, |
|
146 |
) |
|
147 |
response = app.get(endpoint) |
|
148 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site") |
|
149 |
assert mocked_get.call_args[1]["headers"]["token"] == "toktok" |
|
150 |
assert mocked_get.call_count == 1 |
|
151 |
assert "data" in response.json |
|
152 |
assert response.json["err"] == 0 |
|
153 |
for item in response.json["data"]: |
|
154 |
assert "id" in item |
|
155 |
assert "text" in item |
|
156 |
assert "city" in item |
|
157 |
assert "code" in item |
|
158 | ||
159 |
# test cache system |
|
160 |
response = app.get(endpoint) |
|
161 |
assert mocked_get.call_count == 1 |
|
162 | ||
163 |
response = app.get(endpoint + "?q=etangs") |
|
164 |
assert len(response.json["data"]) == 2 |
|
165 |
response = app.get(endpoint + "?q=CourTe") |
|
166 |
assert len(response.json["data"]) == 1 |
|
167 |
response = app.get(endpoint + "?kind=social") |
|
168 |
assert len(response.json["data"]) == 2 |
|
169 | ||
170 |
mocked_get.return_value = utils.FakedResponse( |
|
171 |
content=SD29B_RESPONSE, status_code=200 |
|
172 |
) |
|
173 |
response = app.get(endpoint + "?id=SD29b") |
|
174 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/SD29b") |
|
175 |
assert len(response.json["data"]) == 1 |
|
176 |
assert response.json["data"][0]["id"] == "SD29b" |
|
177 |
assert response.json["data"][0]["dogs"] == "LEASH" |
|
178 | ||
179 |
# bad response for ENS API |
|
180 |
mocked_get.return_value = utils.FakedResponse( |
|
181 |
content=SITE_404_RESPONSE, status_code=404 |
|
182 |
) |
|
183 |
response = app.get(endpoint + "?id=SD29x") |
|
184 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/SD29x") |
|
185 |
assert response.json["err"] == 1 |
|
186 |
assert response.json["err_class"].endswith("APIError") |
|
187 |
assert response.json["err_desc"].startswith("error status:404") |
|
188 |
assert response.json["data"]["status_code"] == 404 |
|
189 |
assert ( |
|
190 |
response.json["data"]["json_content"]["message"] |
|
191 |
== "Site not found with code SD29x" |
|
192 |
) |
|
193 |
mocked_get.return_value = utils.FakedResponse(content="crash", status_code=500) |
|
194 |
response = app.get(endpoint + "?id=foo500") |
|
195 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/foo500") |
|
196 |
assert response.json["err"] == 1 |
|
197 |
assert response.json["err_desc"].startswith("error status:500") |
|
198 |
assert response.json["err_class"].endswith("APIError") |
|
199 |
assert response.json["data"]["status_code"] == 500 |
|
200 |
assert response.json["data"]["json_content"] is None |
|
201 |
mocked_get.return_value = utils.FakedResponse(content=None, status_code=204) |
|
202 |
response = app.get(endpoint + "?id=foo204") |
|
203 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/foo204") |
|
204 |
assert response.json["err"] == 1 |
|
205 |
assert response.json["err_class"].endswith("APIError") |
|
206 |
assert response.json["err_desc"] == "abnormal empty response" |
|
207 |
mocked_get.return_value = utils.FakedResponse(content="not json", status_code=200) |
|
208 |
response = app.get(endpoint + "?id=foo") |
|
209 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/foo") |
|
210 |
assert response.json["err"] == 1 |
|
211 |
assert response.json["err_class"].endswith("APIError") |
|
212 |
assert response.json["err_desc"].startswith("invalid JSON in response:") |
|
213 | ||
214 | ||
215 |
@mock.patch("passerelle.utils.Request.get") |
|
216 |
def test_get_animators(mocked_get, app, setup): |
|
217 |
mocked_get.return_value = utils.FakedResponse( |
|
218 |
content=ANIMATORS_RESPONSE, status_code=200 |
|
219 |
) |
|
220 |
endpoint = reverse( |
|
221 |
"generic-endpoint", |
|
222 |
kwargs={"connector": "isere-ens", "slug": setup.slug, "endpoint": "animators"}, |
|
223 |
) |
|
224 |
response = app.get(endpoint) |
|
225 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/schoolAnimator") |
|
226 |
assert mocked_get.call_count == 1 |
|
227 |
assert "data" in response.json |
|
228 |
assert response.json["err"] == 0 |
|
229 |
for item in response.json["data"]: |
|
230 |
assert "id" in item |
|
231 |
assert "text" in item |
|
232 |
assert "first_name" in item |
|
233 |
assert "email" in item |
|
234 | ||
235 |
# test cache system |
|
236 |
response = app.get(endpoint) |
|
237 |
assert mocked_get.call_count == 1 |
|
238 | ||
239 |
response = app.get(endpoint + "?q=Kael") |
|
240 |
assert len(response.json["data"]) == 1 |
|
241 |
response = app.get(endpoint + "?q=association") |
|
242 |
assert len(response.json["data"]) == 2 |
|
243 |
response = app.get(endpoint + "?q=mail.grd") |
|
244 |
assert len(response.json["data"]) == 2 |
|
245 |
response = app.get(endpoint + "?id=2") |
|
246 |
assert len(response.json["data"]) == 1 |
|
247 |
assert response.json["data"][0]["first_name"] == "Michael" |
|
248 | ||
249 | ||
250 |
@mock.patch("passerelle.utils.Request.get") |
|
251 |
def test_get_site_calendar(mocked_get, app, setup, freezer): |
|
252 |
freezer.move_to("2021-01-21 12:00:00") |
|
253 |
mocked_get.return_value = utils.FakedResponse( |
|
254 |
content=SITE_CALENDAR_RESPONSE, status_code=200 |
|
255 |
) |
|
256 |
endpoint = reverse( |
|
257 |
"generic-endpoint", |
|
258 |
kwargs={ |
|
259 |
"connector": "isere-ens", |
|
260 |
"slug": setup.slug, |
|
261 |
"endpoint": "site-calendar", |
|
262 |
}, |
|
263 |
) |
|
264 |
response = app.get(endpoint + "?site=SD29b") |
|
265 |
assert mocked_get.call_args[0][0].endswith("api/2.0.0/site/SD29b/calendar") |
|
266 |
assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-21" |
|
267 |
assert mocked_get.call_args[1]["params"]["end_date"] == "2021-04-23" |
|
268 |
assert response.json["err"] == 0 |
|
269 |
assert len(response.json["data"]) == 3 |
|
270 |
response = app.get(endpoint + "?site=SD29b&start_date=2021-01-22") |
|
271 |
assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-22" |
|
272 |
assert mocked_get.call_args[1]["params"]["end_date"] == "2021-04-24" |
|
273 |
assert response.json["err"] == 0 |
|
274 |
response = app.get( |
|
275 |
endpoint + "?site=SD29b&start_date=2021-01-22&end_date=2021-01-30" |
|
276 |
) |
|
277 |
assert mocked_get.call_args[1]["params"]["start_date"] == "2021-01-22" |
|
278 |
assert mocked_get.call_args[1]["params"]["end_date"] == "2021-01-30" |
|
279 |
assert response.json["err"] == 0 |
|
280 |
response = app.get(endpoint + "?site=SD29b&start_date=foo", status=400) |
|
281 |
assert response.json["err"] == 1 |
|
282 |
assert response.json["err_class"].endswith("APIError") |
|
283 |
assert ( |
|
284 |
response.json["err_desc"] == "bad start_date format (foo), should be YYYY-MM-DD" |
|
285 |
) |
|
286 |
response = app.get(endpoint + "?site=SD29b&end_date=bar", status=400) |
|
287 |
assert response.json["err"] == 1 |
|
288 |
assert response.json["err_class"].endswith("APIError") |
|
289 |
assert ( |
|
290 |
response.json["err_desc"] == "bad end_date format (bar), should be YYYY-MM-DD" |
|
291 |
) |
|
292 | ||
293 | ||
294 |
@mock.patch("passerelle.utils.Request.post") |
|
295 |
def test_post_book(mocked_post, app, setup): |
|
296 |
mocked_post.return_value = utils.FakedResponse( |
|
297 |
content=BOOK_RESPONSE, status_code=200 |
|
298 |
) |
|
299 |
endpoint = reverse( |
|
300 |
"generic-endpoint", |
|
301 |
kwargs={ |
|
302 |
"connector": "isere-ens", |
|
303 |
"slug": setup.slug, |
|
304 |
"endpoint": "site-booking-school", |
|
305 |
}, |
|
306 |
) |
|
307 |
book = { |
|
308 |
"site": "SD29b", |
|
309 |
"project": "Publik", |
|
310 |
"date": "2020-01-22", |
|
311 |
"pmr": True, |
|
312 |
"morning": True, |
|
313 |
"lunch": False, |
|
314 |
"afternoon": False, |
|
315 |
"participants": "50", |
|
316 |
"animator": "2", |
|
317 |
"school": "1234567X", |
|
318 |
"grade_levels": ["CP", "CE1"], |
|
319 |
"beneficiary_first_name": "Foo", |
|
320 |
"beneficiary_last_name": "Bar", |
|
321 |
"beneficiary_email": "foobar@example.net", |
|
322 |
"beneficiary_phone": "9876", |
|
323 |
"beneficiary_cellphone": "06", |
|
324 |
} |
|
325 |
response = app.post_json(endpoint, params=book) |
|
326 |
assert mocked_post.call_args[0][0].endswith("api/2.0.0/site/booking/school") |
|
327 |
assert mocked_post.call_count == 1 |
|
0 |
- |