From a70537c38cfd0fd2aedc976703b7cf6512ea3d7f Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Fri, 9 Apr 2021 11:39:17 +0200
Subject: [PATCH] opendatasoft: add facet filters (#50212)

---
 passerelle/apps/opendatasoft/forms.py         |  1 +
 .../migrations/0002_auto_20210409_1143.py     | 33 +++++++++++++++++++
 passerelle/apps/opendatasoft/models.py        |  8 +++++
 tests/test_opendatasoft.py                    | 20 +++++++++++
 4 files changed, 62 insertions(+)
 create mode 100644 passerelle/apps/opendatasoft/migrations/0002_auto_20210409_1143.py

diff --git a/passerelle/apps/opendatasoft/forms.py b/passerelle/apps/opendatasoft/forms.py
index a13ddaf1..696aa4b5 100644
--- a/passerelle/apps/opendatasoft/forms.py
+++ b/passerelle/apps/opendatasoft/forms.py
@@ -10,16 +10,17 @@
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU Affero General Public License for more details.
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django import forms
+from django.utils.six.moves.urllib import parse as urlparse
 
 from passerelle.base.forms import BaseQueryFormMixin
 from . import models
 
 
 class QueryForm(BaseQueryFormMixin, forms.ModelForm):
     class Meta:
         model = models.Query
diff --git a/passerelle/apps/opendatasoft/migrations/0002_auto_20210409_1143.py b/passerelle/apps/opendatasoft/migrations/0002_auto_20210409_1143.py
new file mode 100644
index 00000000..ee1354b1
--- /dev/null
+++ b/passerelle/apps/opendatasoft/migrations/0002_auto_20210409_1143.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.18 on 2021-04-09 09:43
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('opendatasoft', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='query',
+            name='filter_expression',
+            field=models.TextField(
+                blank=True,
+                help_text='Specify refine and exclude facet expressions separated by &',
+                verbose_name='filter',
+            ),
+        ),
+        migrations.AlterField(
+            model_name='opendatasoft',
+            name='service_url',
+            field=models.CharField(
+                help_text='URL without ending "api/records/1.0/search/"',
+                max_length=256,
+                verbose_name='Site URL',
+            ),
+        ),
+    ]
diff --git a/passerelle/apps/opendatasoft/models.py b/passerelle/apps/opendatasoft/models.py
index 1be003b9..b5ca342a 100644
--- a/passerelle/apps/opendatasoft/models.py
+++ b/passerelle/apps/opendatasoft/models.py
@@ -88,16 +88,17 @@ class OpenDataSoft(BaseResource):
         params = {
             'dataset': dataset,
             'q': query,
         }
         if self.api_key:
             params.update({'apikey': self.api_key})
         if limit:
             params.update({'rows': limit})
+        params.update(kwargs)  # filter expressions
 
         result_response = self.requests.get(url, params=params)
         err_desc = result_response.json().get('error')
         if err_desc:
             return {'err': 1, 'err_desc': err_desc}
 
         result = []
         for record in result_response.json().get('records'):
@@ -137,21 +138,28 @@ class Query(BaseQuery):
         help_text=_('dataset to query'),
     )
     text_template = models.TextField(
         verbose_name=_('Text template'),
         help_text=_("Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}"),
         validators=[validate_template],
         blank=True,
     )
+    filter_expression = models.TextField(
+        verbose_name=_('filter'),
+        help_text=_('Specify refine and exclude facet expressions separated by &'),
+        blank=True,
+    )
 
     delete_view = 'opendatasoft-query-delete'
     edit_view = 'opendatasoft-query-edit'
 
     def q(self, request, **kwargs):
+        filters = urlparse.parse_qs(self.filter_expression)
+        kwargs.update(filters)
         return self.resource.search(request, dataset=self.dataset, text_template=self.text_template, **kwargs)
 
     def as_endpoint(self):
         endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
 
         search_endpoint = self.resource.search.endpoint_info
         endpoint.func = search_endpoint.func
         endpoint.show_undocumented_params = False
diff --git a/tests/test_opendatasoft.py b/tests/test_opendatasoft.py
index 1f435235..b9da5d9d 100644
--- a/tests/test_opendatasoft.py
+++ b/tests/test_opendatasoft.py
@@ -140,16 +140,17 @@ def connector():
 def query(connector):
     return Query.objects.create(
         resource=connector,
         name='Référenciel adresses de test',
         slug='my_query',
         description='Rechercher une adresse',
         dataset='referentiel-adresse-test',
         text_template='{{numero}} {{nom_rue|safe}} {{nom_commun}}',
+        filter_expression='refine.source=Ville et Eurométropole de Strasbourg&exclude.numero=42&exclude.numero=43',
     )
 
 
 def test_views(db, admin_user, app, connector):
     app = login(app)
     resp = app.get('/opendatasoft/my_connector/', status=200)
     resp = resp.click('New Query')
     resp.form['name'] = 'my query'
@@ -187,16 +188,18 @@ def test_search_empty_contents(mocked_get, app, connector):
 
 @mock.patch('passerelle.utils.Request.get')
 def test_search_using_q(mocked_get, app, connector):
     endpoint = utils.generic_endpoint_url('opendatasoft', 'search', slug=connector.slug)
     assert endpoint == '/opendatasoft/my_connector/search'
     params = {
         'dataset': 'referentiel-adresse-test',
         'text_template': '{{numero}} {{nom_rue|safe}} {{nom_commun}}',
+        'refine.source': 'Ville et Eurométropole de Strasbourg',
+        'exclude.numero': ['42', '43'],
         'q': "rue de l'aubepine",
         'rows': 3,
     }
     mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
     resp = app.get(endpoint, params=params, status=200)
     assert not resp.json['err']
     assert len(resp.json['data']) == 3
     # check order is kept
@@ -217,16 +220,18 @@ def test_search_using_q(mocked_get, app, connector):
 
 @mock.patch('passerelle.utils.Request.get')
 def test_search_using_id(mocked_get, app, connector):
     endpoint = utils.generic_endpoint_url('opendatasoft', 'search', slug=connector.slug)
     assert endpoint == '/opendatasoft/my_connector/search'
     params = {
         'dataset': 'referentiel-adresse-test',
         'text_template': '{{numero}} {{nom_rue|safe}} {{nom_commun}}',
+        'refine.source': 'Ville et Eurométropole de Strasbourg',
+        'exclude.numero': ['42', '43'],
         'id': '7cafcd5c692773e8b863587b2d38d6be82e023d8',
     }
     mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_ID_SEARCH, status_code=200)
     resp = app.get(endpoint, params=params, status=200)
     assert len(resp.json['data']) == 1
     assert resp.json['data'][0]['text'] == "19 RUE DE L'AUBEPINE Lipsheim"
 
 
@@ -234,16 +239,24 @@ def test_search_using_id(mocked_get, app, connector):
 def test_query_q_using_q(mocked_get, app, query):
     endpoint = '/opendatasoft/my_connector/q/my_query/'
     params = {
         'q': "rue de l'aubepine",
         'rows': 3,
     }
     mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_Q_SEARCH, status_code=200)
     resp = app.get(endpoint, params=params, status=200)
+    assert mocked_get.call_args[1]['params'] == {
+        'dataset': 'referentiel-adresse-test',
+        'q': "rue de l'aubepine",
+        'apikey': 'my_secret',
+        'rows': '3',
+        'refine.source': ['Ville et Eurométropole de Strasbourg'],
+        'exclude.numero': ['42', '43'],
+    }
     assert not resp.json['err']
     assert len(resp.json['data']) == 3
     # check order is kept
     assert [x['id'] for x in resp.json['data']] == [
         'e00cf6161e52a4c8fe510b2b74d4952036cb3473',
         '7cafcd5c692773e8b863587b2d38d6be82e023d8',
         '0984a5e1745701f71c91af73ce764e1f7132e0ff',
     ]
@@ -260,16 +273,23 @@ def test_query_q_using_q(mocked_get, app, query):
 @mock.patch('passerelle.utils.Request.get')
 def test_query_q_using_id(mocked_get, app, query):
     endpoint = '/opendatasoft/my_connector/q/my_query/'
     params = {
         'id': '7cafcd5c692773e8b863587b2d38d6be82e023d8',
     }
     mocked_get.return_value = utils.FakedResponse(content=FAKED_CONTENT_ID_SEARCH, status_code=200)
     resp = app.get(endpoint, params=params, status=200)
+    assert mocked_get.call_args[1]['params'] == {
+        'dataset': 'referentiel-adresse-test',
+        'q': 'recordid:7cafcd5c692773e8b863587b2d38d6be82e023d8',
+        'apikey': 'my_secret',
+        'refine.source': ['Ville et Eurométropole de Strasbourg'],
+        'exclude.numero': ['42', '43'],
+    }
     assert len(resp.json['data']) == 1
     assert resp.json['data'][0]['text'] == "19 RUE DE L'AUBEPINE Lipsheim"
 
 
 def test_opendatasoft_query_unicity(admin_user, app, connector, query):
     connector2 = OpenDataSoft.objects.create(
         slug='my_connector2',
         api_key='my_secret',
-- 
2.30.2

