Projet

Général

Profil

0001-add-Isere-ENS-connector-50019.patch

Thomas Noël, 21 janvier 2021 14:33

Télécharger (29,9 ko)

Voir les différences:

Subject: [PATCH] add Isere ENS connector (#50019)

 passerelle/contrib/isere_ens/__init__.py      |   0
 .../isere_ens/migrations/0001_initial.py      |  38 ++
 .../contrib/isere_ens/migrations/__init__.py  |   0
 passerelle/contrib/isere_ens/models.py        | 408 ++++++++++++++++++
 tests/settings.py                             |   1 +
 tests/test_isere_ens.py                       | 334 ++++++++++++++
 6 files changed, 781 insertions(+)
 create mode 100644 passerelle/contrib/isere_ens/__init__.py
 create mode 100644 passerelle/contrib/isere_ens/migrations/0001_initial.py
 create mode 100644 passerelle/contrib/isere_ens/migrations/__init__.py
 create mode 100644 passerelle/contrib/isere_ens/models.py
 create mode 100644 tests/test_isere_ens.py
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
        "code",
39
        "status",
40
        "beneficiary_id",
41
        "beneficiary_first_name",
42
        "beneficiary_last_name",
43
        "beneficiary_email",
44
        "beneficiary_phone",
45
        "beneficiary_cellphone",
46
        "entity_id",
47
        "entity_name",
48
        "entity_type",
49
        "project",
50
        "site",
51
        "applicant",
52
        "public",
53
        "date",
54
        "participants",
55
        "morning",
56
        "lunch",
57
        "afternoon",
58
        "pmr",
59
        "grade_levels",
60
        "animator",
61
    ],
62
    "properties": OrderedDict(
63
        {
64
            "code": {
65
                "description": "booking code",
66
                "type": "string",
67
            },
68
            "status": {
69
                "description": "booking status",
70
                "type": "string",
71
            },
72
            "beneficiary_id": {
73
                "description": "beneficiary id",
74
                "type": "string",
75
            },
76
            "beneficiary_first_name": {
77
                "description": "beneficiary first name",
78
                "type": "string",
79
            },
80
            "beneficiary_last_name": {
81
                "description": "beneficiary last name",
82
                "type": "string",
83
            },
84
            "beneficiary_email": {
85
                "description": "beneficiary email",
86
                "type": "string",
87
            },
88
            "beneficiary_phone": {
89
                "description": "beneficiary phone number",
90
                "type": "string",
91
            },
92
            "beneficiary_cellphone": {
93
                "description": "beneficiary cell phone number",
94
                "type": "string",
95
            },
96
            "entity_id": {
97
                "description": "entity/school id (UAI/RNE)",
98
                "type": "string",
99
            },
100
            "entity_name": {
101
                "description": "entity/school name",
102
                "type": "string",
103
            },
104
            "entity_type": {
105
                "description": "entity/school type",
106
                "type": "string",
107
            },
108
            "project": {
109
                "description": "project code",
110
                "type": "string",
111
            },
112
            "site": {
113
                "description": "site id (code)",
114
                "type": "string",
115
            },
116
            "applicant": {
117
                "description": "applicant",
118
                "type": "string",
119
            },
120
            "public": {
121
                "description": "public",
122
                "type": "string",
123
            },
124
            "date": {
125
                "description": "booking date (format: YYYY-MM-DD)",
126
                "type": "string",
127
            },
128
            "participants": {
129
                "description": "number of participants",
130
                "type": "string",
131
                "pattern": "[0-9]+",
132
            },
133
            "morning": {
134
                "description": "morning booking",
135
                "type": "boolean",
136
            },
137
            "lunch": {
138
                "description": "lunch booking",
139
                "type": "boolean",
140
            },
141
            "afternoon": {
142
                "description": "afternoon booking",
143
                "type": "boolean",
144
            },
145
            "pmr": {
146
                "description": "PMR",
147
                "type": "boolean",
148
            },
149
            "grade_levels": {
150
                "description": "grade levels",
151
                "type": "array",
152
                "items": {
153
                    "type": "string",
154
                    "description": "level",
155
                },
156
            },
157
            "animator": {
158
                "description": "animator id",
159
                "type": "string",
160
                "pattern": "[0-9]+",
161
            },
162
        }
163
    ),
164
}
165

  
166

  
167
class IsereENS(BaseResource, HTTPResource):
168
    category = _("Business Process Connectors")
169

  
170
    base_url = models.URLField(
171
        verbose_name=_("Webservice Base URL"),
172
        help_text=_("Base API URL (before /api/...)"),
173
    )
174
    token = models.CharField(verbose_name=_("Access token"), max_length=128)
175

  
176
    class Meta:
177
        verbose_name = _("Espaces naturels sensibles de l'Isère")
178

  
179
    def request(self, endpoint, params=None, json=None):
180
        url = urlparse.urljoin(self.base_url, endpoint)
181
        headers = {"token": self.token}
182
        if json is not None:
183
            response = self.requests.post(
184
                url, params=params, json=json, headers=headers
185
            )
186
        else:
187
            response = self.requests.get(url, params=params, headers=headers)
188

  
189
        if response.status_code // 100 != 2:
190
            try:
191
                json_content = response.json()
192
            except ValueError:
193
                json_content = None
194
            raise APIError(
195
                "error status:%s %r, content:%r"
196
                % (response.status_code, response.reason, response.content[:1024]),
197
                data={
198
                    "status_code": response.status_code,
199
                    "json_content": json_content,
200
                },
201
            )
202

  
203
        if response.status_code == 204:  # 204 No Content
204
            raise APIError("abnormal empty response")
205

  
206
        try:
207
            return response.json()
208
        except ValueError:
209
            raise APIError("invalid JSON in response: %r" % response.content[:1024])
210

  
211
    @endpoint(
212
        name="sites",
213
        description=_("Sites"),
214
        display_order=1,
215
        perm="can_access",
216
        parameters={
217
            "q": {"description": _("Search text in name field")},
218
            "id": {
219
                "description": _("Returns site with code=id"),
220
            },
221
            "kind": {
222
                "description": _("Select sites by bind: school_group or social"),
223
            },
224
        },
225
    )
226
    def sites(self, request, q=None, id=None, kind=None):
227
        if id is not None:
228
            site = self.request("api/1.0.0/site/" + id)
229
            site["id"] = site["code"]
230
            site["text"] = "%(name)s (%(city)s)" % site
231
            sites = [site]
232
        else:
233
            cache_key = "isere-ens-sites-%d" % self.id
234
            sites = cache.get(cache_key)
235
            if not sites:
236
                sites = self.request("api/1.0.0/site")
237
                for site in sites:
238
                    site["id"] = site["code"]
239
                    site["text"] = "%(name)s (%(city)s)" % site
240
                cache.set(cache_key, sites, 300)
241
        if kind is not None:
242
            sites = [site for site in sites if site.get(kind)]
243
        if q is not None:
244
            q = simplify(q)
245
            sites = [site for site in sites if q in simplify(site["text"])]
246
        return {"data": sites}
247

  
248
    @endpoint(
249
        name="animators",
250
        description=_("Animators"),
251
        display_order=2,
252
        perm="can_access",
253
        parameters={
254
            "q": {"description": _("Search text in name field")},
255
            "id": {
256
                "description": _("Returns animator number id"),
257
            },
258
        },
259
    )
260
    def animators(self, request, q=None, id=None):
261
        cache_key = "isere-ens-animators-%d" % self.id
262
        animators = cache.get(cache_key)
263
        if not animators:
264
            animators = self.request("api/1.0.0/schoolAnimator")
265
            for animator in animators:
266
                animator["id"] = str(animator["id"])
267
                animator["text"] = (
268
                    "%(first_name)s %(last_name)s <%(email)s> (%(entity)s)" % animator
269
                )
270
            cache.set(cache_key, animators, 300)
271
        if id is not None:
272
            animators = [animator for animator in animators if animator["id"] == id]
273
        if q is not None:
274
            q = simplify(q)
275
            animators = [
276
                animator for animator in animators if q in simplify(animator["text"])
277
            ]
278
        return {"data": animators}
279

  
280
    @endpoint(
281
        name="site-calendar",
282
        description=_("Available bookings for a site"),
283
        display_order=3,
284
        perm="can_access",
285
        parameters={
286
            "site": {"description": _("Site code (aka id)")},
287
            "participants": {
288
                "description": _("Number of participants"),
289
            },
290
            "start_date": {
291
                "description": _(
292
                    "First date of the calendar (format: YYYY-MM-DD, default: today)"
293
                ),
294
            },
295
            "end_date": {
296
                "description": _(
297
                    "Last date of the calendar (format: YYYY-MM-DD, default: start_date + 92 days)"
298
                ),
299
            },
300
        },
301
    )
302
    def site_calendar(
303
        self, request, site, participants="1", start_date=None, end_date=None
304
    ):
305
        if start_date:
306
            try:
307
                start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date()
308
            except ValueError:
309
                raise APIError(
310
                    "bad start_date format (%s), should be YYYY-MM-DD" % start_date,
311
                    http_status=400,
312
                )
313
        else:
314
            start_date = datetime.date.today()
315
        if end_date:
316
            try:
317
                end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d").date()
318
            except ValueError:
319
                raise APIError(
320
                    "bad end_date format (%s), should be YYYY-MM-DD" % end_date,
321
                    http_status=400,
322
                )
323
        else:
324
            end_date = start_date + datetime.timedelta(days=92)
325

  
326
        params = {
327
            "site": site,
328
            "participants": participants,
329
            "start_date": start_date.strftime("%Y-%m-%d"),
330
            "end_date": end_date.strftime("%Y-%m-%d"),
331
        }
332
        dates = self.request("api/1.0.0/site/" + site + "/calendar", params=params)
333

  
334
        for date in dates:
335
            date["id"] = site + ":" + date["date"]
336
            date["site"] = site
337
            date_ = datetime.datetime.strptime(date["date"], "%Y-%m-%d").date()
338
            date["date_format"] = date_format(date_, format="DATE_FORMAT")
339
            date["disabled"] = False
340
            date["color"] = "green"
341
            for period in ("morning", "afternoon"):
342
                if date[period] == "COMPLETE":
343
                    date["color"] = "orange"
344
                    date["%s_status" % period] = _("Complete")
345
                else:
346
                    date["%s_status" % period] = _("Available")
347
            if date["morning"] == "COMPLETE" and date["afternoon"] == "COMPLETE":
348
                date["disabled"] = True
349
                date["color"] = "red"
350
            if date["lunch"] == "CLOSE":
351
                date["lunch_status"] = _("Complete")
352
            else:
353
                date["lunch_status"] = _("Available")
354
            date["details"] = _(
355
                "Morning (%(morning_status)s), Lunch (%(lunch_status)s), Afternoon (%(afternoon_status)s)"
356
                % date
357
            )
358
            date["text"] = "%(date_format)s - %(details)s" % date
359
        return {"data": dates}
360

  
361
    @endpoint(
362
        name="site-booking-school",
363
        description=_("Booking a site for a school"),
364
        display_order=4,
365
        perm="can_access",
366
        methods=["post"],
367
        post={
368
            "request_body": {
369
                "schema": {
370
                    "application/json": SITE_BOOKING_SCHOOL_SCHEMA,
371
                }
372
            }
373
        },
374
    )
375
    def site_booking_school(self, request, post_data):
376
        payload = {
377
            "code": post_data["code"],
378
            "status": post_data["status"],
379
            "beneficiary": {
380
                "id": post_data["beneficiary_id"],
381
                "firstName": post_data["beneficiary_first_name"],
382
                "lastName": post_data["beneficiary_last_name"],
383
                "email": post_data["beneficiary_email"],
384
                "phone": post_data["beneficiary_phone"],
385
                "cellphone": post_data["beneficiary_cellphone"],
386
            },
387
            "entity": {
388
                "id": post_data["entity_id"],
389
                "name": post_data["entity_name"],
390
                "type": post_data["entity_type"],
391
            },
392
            "booking": {
393
                "projectCode": post_data.get("project"),
394
                "siteCode": post_data["site"],
395
                "applicant": post_data["applicant"],
396
                "public": post_data["public"],
397
                "bookingDate": post_data["date"],
398
                "participants": int(post_data["participants"]),
399
                "morning": post_data["morning"],
400
                "lunch": post_data["lunch"],
401
                "afternoon": post_data["afternoon"],
402
                "pmr": post_data["pmr"],
403
                "gradeLevels": post_data["grade_levels"],
404
                "schoolAnimator": int(post_data["animator"]),
405
            },
406
        }
407
        booking = self.request("api/1.0.0/booking", json=payload)
408
        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 &eacute;t&eacute; r&eacute;alis&eacute; 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/1.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/1.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/1.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/1.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/1.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/1.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/1.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/1.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
        "code": "resa",
309
        "status": "OK",
310
        "beneficiary_id": "42",
311
        "beneficiary_first_name": "Foo",
312
        "beneficiary_last_name": "Bar",
313
        "beneficiary_email": "foobar@example.net",
314
        "beneficiary_phone": "9876",
315
        "beneficiary_cellphone": "06",
316
        "entity_id": "38420D",
317
        "entity_name": "Ecole FooBar",
318
        "entity_type": "school",
319
        "project": "Publik",
320
        "site": "SD29b",
321
        "applicant": "app",
322
        "public": "GS",
323
        "date": "2020-01-22",
324
        "participants": "50",
325
        "morning": True,
326
        "lunch": False,
327
        "afternoon": False,
328
        "pmr": True,
329
        "grade_levels": ["CP", "CE1"],
330
        "animator": "42",
331
    }
332
    response = app.post_json(endpoint, params=book)
333
    assert mocked_post.call_args[0][0].endswith("api/1.0.0/booking")
334
    assert mocked_post.call_count == 1
0
-