From db2aad04d4c703c2e69f047a37ad91879624eff5 Mon Sep 17 00:00:00 2001
From: Thomas NOEL <tnoel@entrouvert.com>
Date: Fri, 27 Mar 2020 14:49:34 +0100
Subject: [PATCH] okina: handle new API format and endpoint (#40937)

---
 passerelle/apps/okina/models.py               |  50 ++-
 .../okina/templates/okina/okina_detail.html   |   8 +
 tests/test_okina.py                           | 370 +++++++++++++++++-
 3 files changed, 408 insertions(+), 20 deletions(-)

diff --git a/passerelle/apps/okina/models.py b/passerelle/apps/okina/models.py
index 64989d8c..879e2e25 100644
--- a/passerelle/apps/okina/models.py
+++ b/passerelle/apps/okina/models.py
@@ -18,6 +18,7 @@ import json
 
 from django.db import models
 from django.http import HttpResponse
+from django.utils.http import urlencode
 from django.utils.translation import ugettext_lazy as _
 
 from passerelle.base.models import BaseResource
@@ -78,9 +79,13 @@ class Okina(BaseResource):
         okina_cities = self.request('cities')
         cities = []
         for city in okina_cities:
-            city = city['cityObject']
-            city['id'] = '%s' % city['id']
-            city['text'] = '%(nameCity)s (%(zipCode)s)' % city
+            if 'name' in city:
+                city['id'] = '%s' % city['id']
+                city['text'] = '%(name)s (%(zipCode)s)' % city
+            else:  # old API
+                city = city['cityObject']
+                city['id'] = '%s' % city['id']
+                city['text'] = '%(nameCity)s (%(zipCode)s)' % city
             cities.append(city)
         cities.sort(key=lambda x: x['text'])
         return {'data': cities}
@@ -92,8 +97,8 @@ class Okina(BaseResource):
             'text': item['label']
         } for item in self.request('classes')]}
 
-    def get_institutions(self, endpoint=''):
-        okina_institutions = self.request('institutions' + endpoint)
+    def get_institutions(self, query=''):
+        okina_institutions = self.request('institutions' + query)
         institutions = []
         for institution in okina_institutions:
             institution.update(institution['schoolEstablishment'])
@@ -104,14 +109,43 @@ class Okina(BaseResource):
         institutions.sort(key=lambda x: x['text'])
         return institutions
 
-    @endpoint()
-    def institutions(self, request):
-        return {'data': self.get_institutions()}
+    @endpoint(parameters={
+            'insee': {'description': _('INSEE City code'), 'example_value': '36005'},
+        })
+    def institutions(self, request, insee=None):
+        if insee:
+            query = '?' + urlencode({'inseeCode': insee})
+        else:
+            query = ''
+        return {'data': self.get_institutions(query)}
 
     @endpoint(name='institutions', pattern='^from-city/(?P<city_insee_code>\d+)/*$')
     def institutions_from_city(self, request, city_insee_code):
         return {'data': self.get_institutions('/subscriberCity/%s' % city_insee_code)}
 
+    @endpoint(
+            description=_('Get stop points based on a starting position and an arrival institution (API 2020)'),
+            parameters={
+                'lat': {'description': _('Latitude (departure)'), 'example_value': '46.828652'},
+                'lon': {'description': _('Longitude (departure)'), 'example_value': '1.701463'},
+                'address': {'description': _('Address (departure)')},
+                'institution': {'description': _('Institution ID (arrival)'), 'example_value': '277'},
+                'mode': {'description': _('Search mode: CLOSE_SCHOLAR (default) = 3km, FAR_ALL = 15km')},
+            })
+    def search(self, request, lat, lon, institution, address='', mode='CLOSE_SCHOLAR'):
+        payload = {
+                'from-lat': lat,
+                'from-long': lon,
+                'from-address': address,
+                'institution-id': institution,
+                'type': mode,
+        }
+        stops = self.request('wishes/search', payload)
+        for stop in stops:
+            stop['id'] = '%s' % stop['id']
+            stop['text'] = stop['commercial_name']
+        return {'data': stops}
+
     @endpoint(name='stop-areas',
               pattern='^from-city/(?P<city_insee_code>\d+)/to-institution/(?P<institution_id>\d+)/*$')
     def stop_areas(self, request, city_insee_code, institution_id):
diff --git a/passerelle/apps/okina/templates/okina/okina_detail.html b/passerelle/apps/okina/templates/okina/okina_detail.html
index 5427840a..68a229eb 100644
--- a/passerelle/apps/okina/templates/okina/okina_detail.html
+++ b/passerelle/apps/okina/templates/okina/okina_detail.html
@@ -26,6 +26,14 @@ This API provides an access to <a href="https://www.okina.fr/">Okina</a> API.
 
 <h4><span class="description">{% trans 'Journeys' %}</h4>
 <ul class="endpoints">
+<li class="get-method"><span class="description">{% trans 'Get stop points based on a starting position and an arrival institution (API 2020)' %}</span>
+  <a class="example-url" href="search?lat=46.828652&lon=1.701463&institution=277"
+  >{{ site_base_uri }}{{ object.get_absolute_url }}search?lat=...&lon=...&institution=<i>institution_id</i></a></li>
+
+<li class="get-method"><span class="description">{% trans 'Institutions in a city (API 2020)' %}</span>
+  <a class="example-url" href="institutions?insee=36005"
+  >{{ site_base_uri }}{{ object.get_absolute_url }}institutions?insee=<i>city-insee-code</i></a></li>
+
 <li class="get-method"><span class="description">{% trans 'Institutions accessible from a city:' %}</span>
   <a class="example-url" href="institutions/from-city/36005/"
   >{{ site_base_uri }}{{ object.get_absolute_url }}institutions/from-city/<i>city-insee-code</i>/</a></li>
diff --git a/tests/test_okina.py b/tests/test_okina.py
index 468255cf..c5e2a101 100644
--- a/tests/test_okina.py
+++ b/tests/test_okina.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+import json
 import pytest
 import mock
 import utils
@@ -33,6 +34,22 @@ CITIES = '''[ {
 
 '''
 
+# new API (march 2020)
+CITIES_NEW = '''[
+  {
+    "id": 83355,
+    "insee": "36005",
+    "name": "ARDENTES",
+    "zipCode": "36120",
+    "nameCityRouting": null,
+    "line5": null,
+    "latitude": null,
+    "longitude": null,
+    "geoJson": null
+  }
+]
+'''
+
 CLASSES = '''
 [
   {
@@ -94,6 +111,297 @@ INSTITUTIONS = '''[ {
 
 '''
 
+# new API (march 2020)
+INSTITUTIONS_NEW = '''[ {
+  "id" : 276,
+  "schoolEstablishment" : {
+    "uai" : "0360155Y",
+    "uaiLabel" : "Ecole élémentaire Saint-Martin",
+    "type" : "Public",
+    "address" : "Rue Calmette et Guérin",
+    "zipCode" : "36120",
+    "codeCity" : "36005",
+    "cityName" : "Ardentes",
+    "locationX" : "1.8296541547236063",
+    "locationY" : "46.74024796567764",
+    "nature" : "1000",
+    "natureTitle" : "Ecole",
+    "restoration" : true,
+    "accommodation" : false,
+    "ulis" : false,
+    "segpa" : false,
+    "sirenSiret" : "21360005900033",
+    "webSite" : null,
+    "phone" : "254362184",
+    "fax" : "",
+    "email" : "ec-saint-martin-ardentes@ac-orleans-tours.fr",
+    "departmentLabel" : "Indre",
+    "academyLabel" : "Orléans-Tours",
+    "regionLabel" : "Centre-Val de Loire",
+    "schoolSectionList" : [ ],
+    "schoolEstablishmentTypeInfos" : [ {
+      "key" : "ecole_elementaire",
+      "value" : "École élémentaire"
+    } ]
+  },
+  "schoolEstablishmentDisplayName" : "Ecole élémentaire Saint-Martin",
+  "schoolEstablishmentUai" : "0360155Y",
+  "zipAndCity" : "36120 - Ardentes",
+  "withContract" : true,
+  "institutionContacts" : [ ],
+  "institutionTimes" : [ {
+    "id" : 27,
+    "day" : "MONDAY",
+    "way" : "WAY_TO",
+    "time" : "08:46:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 28,
+    "day" : "MONDAY",
+    "way" : "WAY_BACK",
+    "time" : "16:30:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 29,
+    "day" : "TUESDAY",
+    "way" : "WAY_TO",
+    "time" : "08:46:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 30,
+    "day" : "TUESDAY",
+    "way" : "WAY_BACK",
+    "time" : "16:30:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 31,
+    "day" : "THURSDAY",
+    "way" : "WAY_TO",
+    "time" : "08:46:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 32,
+    "day" : "THURSDAY",
+    "way" : "WAY_BACK",
+    "time" : "16:30:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 33,
+    "day" : "FRIDAY",
+    "way" : "WAY_TO",
+    "time" : "08:46:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 34,
+    "day" : "FRIDAY",
+    "way" : "WAY_BACK",
+    "time" : "16:30:00",
+    "institutionId" : 0,
+    "timeString" : null
+  } ],
+  "cities" : null,
+  "optionsDerogatory" : [ ],
+  "institutionCenterName" : "ARDENTES",
+  "institutionCenterId" : 3,
+  "customLocationX" : "1.8296541547236063",
+  "customLocationY" : "46.74024796567764"
+}, {
+  "id" : 277,
+  "schoolEstablishment" : {
+    "uai" : "0360048G",
+    "uaiLabel" : "Collège Touvent",
+    "type" : "Public",
+    "address" : "4 allée des Lauriers",
+    "zipCode" : "36000",
+    "codeCity" : "36044",
+    "cityName" : "Châteauroux",
+    "locationX" : "1.6926355997775921",
+    "locationY" : "46.791332586468016",
+    "nature" : "340",
+    "natureTitle" : "Collège",
+    "restoration" : false,
+    "accommodation" : false,
+    "ulis" : true,
+    "segpa" : false,
+    "sirenSiret" : "19360048300013",
+    "webSite" : null,
+    "phone" : "02 54 34 03 30",
+    "fax" : "02 54 08 01 09",
+    "email" : "ce.0360048g@ac-orleans-tours.fr",
+    "departmentLabel" : "Indre",
+    "academyLabel" : "Orléans-Tours",
+    "regionLabel" : "Centre-Val de Loire",
+    "schoolSectionList" : [ {
+      "key" : "section_sport",
+      "name" : "Section sport"
+    } ],
+    "schoolEstablishmentTypeInfos" : [ ]
+  },
+  "schoolEstablishmentDisplayName" : "Collège Touvent",
+  "schoolEstablishmentUai" : "0360048G",
+  "zipAndCity" : "36000 - Châteauroux",
+  "withContract" : true,
+  "institutionContacts" : [ ],
+  "institutionTimes" : [ {
+    "id" : 249,
+    "day" : "MONDAY",
+    "way" : "WAY_TO",
+    "time" : "08:20:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 250,
+    "day" : "MONDAY",
+    "way" : "WAY_BACK",
+    "time" : "17:05:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 251,
+    "day" : "TUESDAY",
+    "way" : "WAY_TO",
+    "time" : "08:20:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 252,
+    "day" : "TUESDAY",
+    "way" : "WAY_BACK",
+    "time" : "17:05:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 253,
+    "day" : "WEDNESDAY",
+    "way" : "WAY_TO",
+    "time" : "07:50:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 254,
+    "day" : "WEDNESDAY",
+    "way" : "WAY_BACK",
+    "time" : "12:05:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 255,
+    "day" : "THURSDAY",
+    "way" : "WAY_TO",
+    "time" : "08:20:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 256,
+    "day" : "THURSDAY",
+    "way" : "WAY_BACK",
+    "time" : "17:05:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 257,
+    "day" : "FRIDAY",
+    "way" : "WAY_TO",
+    "time" : "08:20:00",
+    "institutionId" : 0,
+    "timeString" : null
+  }, {
+    "id" : 258,
+    "day" : "FRIDAY",
+    "way" : "WAY_BACK",
+    "time" : "17:05:00",
+    "institutionId" : 0,
+    "timeString" : null
+  } ],
+  "cities" : null,
+  "optionsDerogatory" : [ ],
+  "institutionCenterName" : "CHATEAUROUX",
+  "institutionCenterId" : 2,
+  "customLocationX" : "1.6926355997775921",
+  "customLocationY" : "46.791332586468016"
+} ]
+'''
+
+SEARCH = '''[ {
+  "id" : 3312,
+  "stop_point_id" : 1498,
+  "name" : "Le Grand Verger",
+  "latitude" : 46.8444186,
+  "longitude" : 1.708197,
+  "bearing" : 0.0,
+  "position" : 6,
+  "forBoarding" : null,
+  "forAlighting" : null,
+  "colorUsage" : null,
+  "isRecommended" : false,
+  "titreRelaisId" : null,
+  "networkType" : "SCHOLAR",
+  "commercial_stop_area" : 3312,
+  "commercial_name" : "Le Grand Verger",
+  "liaisons" : [ ],
+  "registrationNumber" : "36063_009",
+  "cityCode" : "36063",
+  "cityName" : "Déols",
+  "zipCode" : "36130",
+  "isDuplicatedCompute" : 0,
+  "nbClosedStopAreas" : null,
+  "isValidated" : null,
+  "isUnique" : null,
+  "isDuplicated" : null,
+  "network" : {
+    "id" : 2,
+    "name" : "réseau scolaire",
+    "supprime" : false,
+    "networkType" : "SCHOLAR"
+  },
+  "external_ref" : null,
+  "distanceBetweenPreviousStopCalculated" : 0.0,
+  "distanceBetweenPreviousStopReal" : null
+}, {
+  "id" : 3311,
+  "stop_point_id" : 1499,
+  "name" : "Jean Bizet U",
+  "latitude" : 46.8359017,
+  "longitude" : 1.7045139,
+  "bearing" : 0.0,
+  "position" : 7,
+  "forBoarding" : null,
+  "forAlighting" : null,
+  "colorUsage" : null,
+  "isRecommended" : true,
+  "titreRelaisId" : null,
+  "networkType" : "SCHOLAR",
+  "commercial_stop_area" : 3311,
+  "commercial_name" : "Jean Bizet U",
+  "liaisons" : [ ],
+  "registrationNumber" : "36063_008",
+  "cityCode" : "36063",
+  "cityName" : "Déols",
+  "zipCode" : "36130",
+  "isDuplicatedCompute" : 0,
+  "nbClosedStopAreas" : null,
+  "isValidated" : null,
+  "isUnique" : null,
+  "isDuplicated" : null,
+  "network" : {
+    "id" : 2,
+    "name" : "réseau scolaire",
+    "supprime" : false,
+    "networkType" : "SCHOLAR"
+  },
+  "external_ref" : null,
+  "distanceBetweenPreviousStopCalculated" : 0.0,
+  "distanceBetweenPreviousStopReal" : null
+} ]
+'''
+
 STOPS = '''
 [ {
   "id" : 3281,
@@ -1356,6 +1664,17 @@ def test_okina_cities(app, okina):
         assert resp.json['data'][0]['insee'] == '36005'
         assert resp.json['data'][0]['text'] == 'ARDENTES (36120)'
 
+        requests_get.return_value = utils.FakedResponse(content=CITIES_NEW,
+                                                        status_code=200)
+        resp = app.get(endpoint, status=200)
+        assert requests_get.call_count == 2
+        assert 'data' in resp.json
+        assert resp.json['err'] == 0
+        assert len(resp.json['data']) == 1
+        assert resp.json['data'][0]['id'] == '83355'
+        assert resp.json['data'][0]['insee'] == '36005'
+        assert resp.json['data'][0]['text'] == 'ARDENTES (36120)'
+
 def test_okina_classes(app, okina):
     endpoint = utils.generic_endpoint_url('okina', 'classes', slug=okina.slug)
     assert endpoint == '/okina/test/classes'
@@ -1374,21 +1693,48 @@ def test_okina_institutions(app, okina):
     endpoint = utils.generic_endpoint_url('okina', 'institutions', slug=okina.slug)
     assert endpoint == '/okina/test/institutions'
     with mock.patch('passerelle.utils.Request.get') as requests_get:
-        requests_get.return_value = utils.FakedResponse(content=INSTITUTIONS,
-                                                        status_code=200)
-        resp = app.get(endpoint, status=200)
-        assert requests_get.call_args[0][0] == 'https://okina.example.net/b2b/institutions'
-        assert resp.json['err'] == 0
-        assert len(resp.json['data']) == 2
-        assert resp.json['data'][0]['id'] == '277'
-        assert resp.json['data'][0]['text'] == u'Collège Touvent'
+        for content in (INSTITUTIONS, INSTITUTIONS_NEW):
+            requests_get.return_value = utils.FakedResponse(content=content, status_code=200)
+            resp = app.get(endpoint, status=200)
+            assert requests_get.call_args[0][0] == 'https://okina.example.net/b2b/institutions'
+            assert resp.json['err'] == 0
+            assert len(resp.json['data']) == 2
+            assert resp.json['data'][0]['id'] == '277'
+            assert resp.json['data'][0]['text'] == u'Collège Touvent'
 
-        resp = app.get(endpoint + '/from-city/36005/', status=200)
-        assert requests_get.call_args[0][0] == 'https://okina.example.net/b2b/institutions/subscriberCity/36005'
+            resp = app.get(endpoint, params={'insee': '36005'}, status=200)
+            assert requests_get.call_args[0][0] == 'https://okina.example.net/b2b/institutions?inseeCode=36005'
+            assert resp.json['err'] == 0
+            assert len(resp.json['data']) == 2
+            assert resp.json['data'][0]['id'] == '277'
+            assert resp.json['data'][0]['text'] == u'Collège Touvent'
+
+            resp = app.get(endpoint + '/from-city/36005/', status=200)
+            assert requests_get.call_args[0][0] == 'https://okina.example.net/b2b/institutions/subscriberCity/36005'
+            assert resp.json['err'] == 0
+            assert len(resp.json['data']) == 2
+            assert resp.json['data'][0]['id'] == '277'
+            assert resp.json['data'][0]['text'] == u'Collège Touvent'
+
+def test_okina_search(app, okina):
+    endpoint = utils.generic_endpoint_url('okina', 'search', slug=okina.slug)
+    assert endpoint == '/okina/test/search'
+    with mock.patch('passerelle.utils.Request.post') as requests_post:
+        requests_post.return_value = utils.FakedResponse(content=SEARCH, status_code=200)
+        app.get(endpoint + '?lat=46.828652', status=400)  # missing argument
+        resp = app.get(endpoint + '?lat=46.828652&lon=1.701463&institution=277', status=200)
+        assert requests_post.call_args[0][0] == 'https://okina.example.net/b2b/wishes/search'
+        assert json.loads(requests_post.call_args[1]['data']) == {
+            "type": "CLOSE_SCHOLAR",
+            "from-address": "",
+            "from-lat": "46.828652",
+            "from-long": "1.701463",
+            "institution-id": "277"
+        }
         assert resp.json['err'] == 0
         assert len(resp.json['data']) == 2
-        assert resp.json['data'][0]['id'] == '277'
-        assert resp.json['data'][0]['text'] == u'Collège Touvent'
+        assert resp.json['data'][0]['id'] == '3312'
+        assert resp.json['data'][0]['text'] == 'Le Grand Verger'
 
 def test_okina_stops_area(app, okina):
     endpoint = utils.generic_endpoint_url('okina', 'stop-areas', slug=okina.slug)
-- 
2.26.2

