From e3e1fed23d271486dcc992692e465eee22f8db23 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Thu, 21 Nov 2019 18:52:01 +1100 Subject: [PATCH 1/2] Allow relationships to be compared to scalars --- .../data_layers/filtering/alchemy.py | 4 ++++ flask_rest_jsonapi/resource.py | 2 +- tests/test_sqlalchemy_data_layer.py | 23 ++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/flask_rest_jsonapi/data_layers/filtering/alchemy.py b/flask_rest_jsonapi/data_layers/filtering/alchemy.py index d615343..6c683f9 100644 --- a/flask_rest_jsonapi/data_layers/filtering/alchemy.py +++ b/flask_rest_jsonapi/data_layers/filtering/alchemy.py @@ -3,6 +3,7 @@ """Helper to create sqlalchemy filters according to filter querystring parameter""" from sqlalchemy import and_, or_, not_ +from sqlalchemy.orm import RelationshipProperty from flask_rest_jsonapi.exceptions import InvalidFilters from flask_rest_jsonapi.schema import get_relationships, get_model_field @@ -51,6 +52,9 @@ def resolve(self): if isinstance(value, dict): return getattr(self.column, self.operator)(**value) + elif isinstance(self.column.prop, RelationshipProperty) and isinstance(value, (int, str)): + relationship_key = next(iter(self.column.prop.local_columns)) + return getattr(relationship_key, self.operator)(value) else: return getattr(self.column, self.operator)(value) diff --git a/flask_rest_jsonapi/resource.py b/flask_rest_jsonapi/resource.py index 34ceccf..72ac67e 100644 --- a/flask_rest_jsonapi/resource.py +++ b/flask_rest_jsonapi/resource.py @@ -134,7 +134,7 @@ def get(self, *args, **kwargs): add_pagination_links(result, objects_count, qs, - url_for(self.view, _external=True, **view_kwargs)) + url_for(request.endpoint, _external=True, **view_kwargs)) result.update({'meta': {'count': objects_count}}) diff --git a/tests/test_sqlalchemy_data_layer.py b/tests/test_sqlalchemy_data_layer.py index ea40923..0d8f92b 100644 --- a/tests/test_sqlalchemy_data_layer.py +++ b/tests/test_sqlalchemy_data_layer.py @@ -373,7 +373,7 @@ def get(self): @pytest.fixture(scope="module") -def query(): +def query(computer_model, person_model): def query_(self, view_kwargs): if view_kwargs.get('person_id') is not None: return self.session.query(computer_model).join(person_model).filter_by(person_id=view_kwargs['person_id']) @@ -641,6 +641,26 @@ def test_get_list_with_simple_filter(client, register_routes, person, person_2): response = client.get('/persons' + '?' + querystring, content_type='application/vnd.api+json') assert response.status_code == 200 +def test_get_list_relationship_filter(client, register_routes, person_computers, + computer_schema, person, person_2, computer, session): + """ + Tests the ability to filter over a relationship using IDs, for example + `GET /comments?filter[post]=1` + Refer to the spec: https://jsonapi.org/recommendations/#filtering + """ + # We have two people in the database, one of which owns a computer + person.computers.append(computer) + session.commit() + + querystring = urlencode({ + 'filter[owner]': person.person_id + }) + response = client.get('/computers?' + querystring, content_type='application/vnd.api+json') + + # Check that the request worked, and returned the one computer we wanted + assert response.status_code == 200 + assert len(response.json['data']) == 1 + def test_get_list_disable_pagination(client, register_routes): with client: querystring = urlencode({'page[size]': 0}) @@ -1804,3 +1824,4 @@ def test_api_resources(app, person_list): def test_relationship_containing_hyphens(client, register_routes, person_computers, computer_schema, person): response = client.get('/persons/{}/relationships/computers-owned'.format(person.person_id), content_type='application/vnd.api+json') assert response.status_code == 200 + From 8ec2aad21653c7b11e2e0c05f02647647f0473b7 Mon Sep 17 00:00:00 2001 From: Michael Milton Date: Sat, 23 Nov 2019 00:08:37 +1100 Subject: [PATCH 2/2] Parse lists of simple filters using in_ operator --- flask_rest_jsonapi/querystring.py | 14 ++++++++++++-- tests/test_sqlalchemy_data_layer.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/flask_rest_jsonapi/querystring.py b/flask_rest_jsonapi/querystring.py index 7240071..620bf5d 100644 --- a/flask_rest_jsonapi/querystring.py +++ b/flask_rest_jsonapi/querystring.py @@ -61,8 +61,18 @@ def _get_key_values(self, name): return results def _simple_filters(self, dict_): - return [{"name": key, "op": "eq", "val": value} - for (key, value) in dict_.items()] + ret = [] + + # Since the value of the filter can be a list, we have to choose the right + # operator dynamically + for key, value in dict_.items(): + if isinstance(value, list): + op = 'in_' + else: + op = 'eq' + + ret.append({"name": key, "op": op, "val": value}) + return ret @property def querystring(self): diff --git a/tests/test_sqlalchemy_data_layer.py b/tests/test_sqlalchemy_data_layer.py index 0d8f92b..aab18c7 100644 --- a/tests/test_sqlalchemy_data_layer.py +++ b/tests/test_sqlalchemy_data_layer.py @@ -661,6 +661,22 @@ def test_get_list_relationship_filter(client, register_routes, person_computers, assert response.status_code == 200 assert len(response.json['data']) == 1 +def test_get_list_filter_many(client, register_routes, person_computers, + computer_schema, person, person_2, session, computer_model): + """ + Tests the ability to filter using a list of IDs + `GET /comments?filter[id]=1,2` + Refer to the spec: https://jsonapi.org/recommendations/#filtering + """ + querystring = urlencode({ + 'filter[id]': '{},{}'.format(person.person_id, person_2.person_id) + }) + response = client.get('/persons?' + querystring, content_type='application/vnd.api+json') + + # Check that the request worked, and returned both the users we specified + assert response.status_code == 200, response.json['errors'] + assert len(response.json['data']) == 2 + def test_get_list_disable_pagination(client, register_routes): with client: querystring = urlencode({'page[size]': 0})