Skip to content

Commit 6e5424d

Browse files
committed
Ability to use custom query string manager
- documentation - tests
1 parent dfe00fb commit 6e5424d

File tree

4 files changed

+165
-29
lines changed

4 files changed

+165
-29
lines changed

docs/resource_manager.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ Flask-COMBO-JSONAPI provides three kinds of resource managers with default metho
1515

1616
You can rewrite each default method implementation to customize it. If you rewrite all default methods of a resource manager or if you rewrite a method and disable access to others, you don't have to set any attributes of your resource manager.
1717

18+
All url are pased via helper class **QueryStringManager**, which make parsing url query string according json-api. If you want override implementation class used you can do it for 1 resource via attribute.
19+
:qs_manager_class: default implementation via **QueryStringManager**
20+
21+
or globally via:
22+
.. code-block::python
23+
24+
api = Api(blueprint=api_blueprint, qs_manager_class=CustomQS)
25+
1826
Required attributes
1927
-------------------
2028

flask_combo_jsonapi/api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
class Api(object):
1616
"""The main class of the Api"""
1717

18-
def __init__(self, app=None, blueprint=None, decorators=None, plugins=None):
18+
def __init__(self, app=None, blueprint=None, decorators=None, plugins=None, qs_manager_class=None):
1919
"""Initialize an instance of the Api
2020
2121
:param app: the flask application
2222
:param blueprint: a flask blueprint
2323
:param tuple decorators: a tuple of decorators plugged to each resource methods
24+
:param plugins: list of plugins
25+
:param qs_manager_class: custom query string manager used in whole API
2426
"""
2527
self.app = app
2628
self._app = app
@@ -29,6 +31,7 @@ def __init__(self, app=None, blueprint=None, decorators=None, plugins=None):
2931
self.resource_registry = []
3032
self.decorators = decorators or tuple()
3133
self.plugins = plugins if plugins is not None else []
34+
self.qs_manager_class = qs_manager_class
3235

3336
if app is not None:
3437
self.init_app(app, blueprint)
@@ -82,6 +85,9 @@ def route(self, resource, view, *urls, **kwargs):
8285
pass
8386
setattr(resource, 'plugins', self.plugins)
8487

88+
if self.qs_manager_class:
89+
setattr(resource, 'qs_manager_class', self.qs_manager_class)
90+
8591
resource.view = view
8692
url_rule_options = kwargs.get('url_rule_options') or dict()
8793

flask_combo_jsonapi/resource.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def __new__(cls, name, bases, d):
5252
class Resource(MethodView):
5353
"""Base resource class"""
5454

55+
qs_manager_class = QSManager
56+
5557
def __new__(cls):
5658
"""Constructor of a resource instance"""
5759
if hasattr(cls, "_data_layer"):
@@ -117,7 +119,7 @@ def get(self, *args, **kwargs):
117119
"""Retrieve a collection of objects"""
118120
self.before_get(args, kwargs)
119121

120-
qs = QSManager(request.args, self.schema)
122+
qs = self.qs_manager_class(request.args, self.schema)
121123

122124
objects_count, objects = self.get_collection(qs, kwargs)
123125

@@ -152,7 +154,7 @@ def post(self, *args, **kwargs):
152154
"""Create an object"""
153155
json_data = request.json or {}
154156

155-
qs = QSManager(request.args, self.schema)
157+
qs = self.qs_manager_class(request.args, self.schema)
156158

157159
schema = compute_schema(self.schema, getattr(self, "post_schema_kwargs", dict()), qs, qs.include)
158160

@@ -231,7 +233,7 @@ def get(self, *args, **kwargs):
231233
"""Get object details"""
232234
self.before_get(args, kwargs)
233235

234-
qs = QSManager(request.args, self.schema)
236+
qs = self.qs_manager_class(request.args, self.schema)
235237

236238
obj = self.get_object(kwargs, qs)
237239

@@ -263,7 +265,7 @@ def patch(self, *args, **kwargs):
263265
"""Update an object"""
264266
json_data = request.json or {}
265267

266-
qs = QSManager(request.args, self.schema)
268+
qs = self.qs_manager_class(request.args, self.schema)
267269
schema_kwargs = getattr(self, "patch_schema_kwargs", dict())
268270
schema_kwargs.update({"partial": True})
269271

@@ -385,7 +387,7 @@ def get(self, *args, **kwargs):
385387
"data": data,
386388
}
387389

388-
qs = QSManager(request.args, self.schema)
390+
qs = self.qs_manager_class(request.args, self.schema)
389391
if qs.include:
390392
schema = compute_schema(self.schema, dict(), qs, qs.include)
391393

tests/test_sqlalchemy_data_layer.py

Lines changed: 143 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def engine(
158158
person_tag_model, person_single_tag_model, person_model,
159159
computer_model, string_json_attribute_person_model,
160160
address_model
161-
):
161+
):
162162
engine = create_engine("sqlite:///:memory:")
163163
person_tag_model.metadata.create_all(engine)
164164
person_single_tag_model.metadata.create_all(engine)
@@ -208,6 +208,17 @@ def computer(session, computer_model):
208208
session_.commit()
209209

210210

211+
@pytest.fixture()
212+
def computer_2(session, computer_model):
213+
computer_ = computer_model(serial="2")
214+
session_ = session
215+
session_.add(computer_)
216+
session_.commit()
217+
yield computer_
218+
session_.delete(computer_)
219+
session_.commit()
220+
221+
211222
@pytest.fixture()
212223
def address(session, address_model):
213224
address_ = address_model(state='NYC')
@@ -400,7 +411,7 @@ class PersonList(ResourceList):
400411
data_layer = {
401412
"model": person_model,
402413
"session": session,
403-
"mzthods": {"before_create_object": before_create_object},
414+
"methods": {"before_create_object": before_create_object},
404415
}
405416
get_decorators = [dummy_decorator]
406417
post_decorators = [dummy_decorator]
@@ -410,6 +421,43 @@ class PersonList(ResourceList):
410421
yield PersonList
411422

412423

424+
@pytest.fixture(scope="module")
425+
def custom_query_string_manager():
426+
class QS(QSManager):
427+
def _simple_filters(self, dict_):
428+
return [{"name": key, "op": "in" if isinstance(value, list) else "eq", "val": value}
429+
for (key, value) in dict_.items()]
430+
431+
yield QS
432+
433+
434+
@pytest.fixture(scope="module")
435+
def person_list_custom_qs_manager(session, person_model, person_schema, custom_query_string_manager):
436+
class PersonList(ResourceList):
437+
schema = person_schema
438+
data_layer = {
439+
"model": person_model,
440+
"session": session,
441+
}
442+
get_schema_kwargs = dict()
443+
qs_manager_class = custom_query_string_manager
444+
445+
yield PersonList
446+
447+
448+
@pytest.fixture(scope="module")
449+
def person_list_2(session, person_model, person_schema):
450+
class PersonList(ResourceList):
451+
schema = person_schema
452+
data_layer = {
453+
"model": person_model,
454+
"session": session,
455+
}
456+
get_schema_kwargs = dict()
457+
458+
yield PersonList
459+
460+
413461
@pytest.fixture(scope="module")
414462
def person_detail(session, person_model, dummy_decorator, person_schema, before_update_object, before_delete_object):
415463
class PersonDetail(ResourceDetail):
@@ -507,7 +555,7 @@ def fixed_count_for_collection_count():
507555

508556
@pytest.fixture(scope="module")
509557
def computer_list_resource_with_disable_collection_count(
510-
session, computer_model, computer_schema, fixed_count_for_collection_count
558+
session, computer_model, computer_schema, fixed_count_for_collection_count
511559
):
512560
class ComputerList(ResourceList):
513561
disable_collection_count = True, fixed_count_for_collection_count
@@ -538,7 +586,7 @@ class ComputerOwnerRelationship(ResourceRelationship):
538586

539587
@pytest.fixture(scope="module")
540588
def string_json_attribute_person_detail(
541-
session, string_json_attribute_person_model, string_json_attribute_person_schema
589+
session, string_json_attribute_person_model, string_json_attribute_person_schema
542590
):
543591
class StringJsonAttributePersonDetail(ResourceDetail):
544592
schema = string_json_attribute_person_schema
@@ -562,27 +610,42 @@ def api_blueprint(client):
562610
yield bp
563611

564612

613+
@pytest.fixture(scope="module")
614+
def register_routes_custom_qs(
615+
client,
616+
app,
617+
api_blueprint,
618+
custom_query_string_manager,
619+
person_list_2,
620+
):
621+
api = Api(blueprint=api_blueprint, qs_manager_class=custom_query_string_manager)
622+
api.route(person_list_2, "person_list_qs", "/qs/persons")
623+
api.init_app(app)
624+
625+
565626
@pytest.fixture(scope="module")
566627
def register_routes(
567-
client,
568-
app,
569-
api_blueprint,
570-
person_list,
571-
person_detail,
572-
person_computers,
573-
person_list_raise_jsonapiexception,
574-
person_list_raise_exception,
575-
person_list_response,
576-
person_list_without_schema,
577-
computer_list,
578-
computer_detail,
579-
computer_list_resource_with_disable_collection_count,
580-
computer_owner,
581-
string_json_attribute_person_detail,
582-
string_json_attribute_person_list,
628+
client,
629+
app,
630+
api_blueprint,
631+
person_list,
632+
person_detail,
633+
person_computers,
634+
person_list_custom_qs_manager,
635+
person_list_raise_jsonapiexception,
636+
person_list_raise_exception,
637+
person_list_response,
638+
person_list_without_schema,
639+
computer_list,
640+
computer_detail,
641+
computer_list_resource_with_disable_collection_count,
642+
computer_owner,
643+
string_json_attribute_person_detail,
644+
string_json_attribute_person_list,
583645
):
584646
api = Api(blueprint=api_blueprint)
585647
api.route(person_list, "person_list", "/persons")
648+
api.route(person_list_custom_qs_manager, "person_list_custom_qs_manager", "/persons_qs")
586649
api.route(person_detail, "person_detail", "/persons/<int:person_id>")
587650
api.route(person_computers, "person_computers", "/persons/<int:person_id>/relationships/computers")
588651
api.route(person_computers, "person_computers_owned", "/persons/<int:person_id>/relationships/computers-owned")
@@ -775,6 +838,47 @@ def test_get_list_with_simple_filter(client, register_routes, person, person_2):
775838
)
776839
response = client.get("/persons" + "?" + querystring, content_type="application/vnd.api+json")
777840
assert response.status_code == 200
841+
assert response.json["meta"]["count"] == 1
842+
843+
844+
def test_get_list_with_simple_filter_relationship_custom_qs(session, client, register_routes, person, person_2,
845+
computer, computer_2):
846+
computer.person = person
847+
computer_2.person = person_2
848+
session.commit()
849+
with client:
850+
querystring = urlencode(
851+
{
852+
"filter[computers.id]": f'{computer_2.id},{computer.id}',
853+
"include": "computers",
854+
"sort": "-name",
855+
}
856+
)
857+
response = client.get("/persons_qs" + "?" + querystring, content_type="application/vnd.api+json")
858+
assert response.status_code == 200
859+
assert len(response.json['data']) == 2
860+
assert response.json['data'][0]['id'] == str(person_2.person_id)
861+
assert response.json['data'][1]['id'] == str(person.person_id)
862+
863+
864+
def test_get_list_with_simple_filter_relationship_custom_qs_api(session, client, register_routes_custom_qs, person,
865+
person_2, computer, computer_2):
866+
computer.person = person
867+
computer_2.person = person_2
868+
session.commit()
869+
with client:
870+
querystring = urlencode(
871+
{
872+
"filter[computers.id]": f'{computer_2.id},{computer.id}',
873+
"include": "computers",
874+
"sort": "-name",
875+
}
876+
)
877+
response = client.get("/qs/persons" + "?" + querystring, content_type="application/vnd.api+json")
878+
assert response.status_code == 200
879+
assert len(response.json['data']) == 2
880+
assert response.json['data'][0]['id'] == str(person_2.person_id)
881+
assert response.json['data'][1]['id'] == str(person.person_id)
778882

779883

780884
def test_get_list_disable_pagination(client, register_routes):
@@ -1154,6 +1258,22 @@ class PersonDetail(ResourceDetail):
11541258
PersonDetail()
11551259

11561260

1261+
def test_get_list_with_simple_filter_relationship_error(session, client, register_routes, person, person_2,
1262+
computer, computer_2):
1263+
computer.person = person
1264+
computer_2.person = person_2
1265+
session.commit()
1266+
with client:
1267+
querystring = urlencode(
1268+
{
1269+
"filter[computers.id]": f'{computer_2.id},{computer.id}',
1270+
"include": "computers"
1271+
}
1272+
)
1273+
response = client.get("/persons" + "?" + querystring, content_type="application/vnd.api+json")
1274+
assert response.status_code == 500
1275+
1276+
11571277
def test_get_list_jsonapiexception(client, register_routes):
11581278
with client:
11591279
response = client.get("/persons_jsonapiexception", content_type="application/vnd.api+json")
@@ -1547,7 +1667,7 @@ def test_post_relationship_missing_type(client, register_routes, computer, perso
15471667

15481668

15491669
def test_post_relationship_missing_id(client, register_routes, computer, person):
1550-
payload = {"data": [{"type": "computer",}]}
1670+
payload = {"data": [{"type": "computer", }]}
15511671

15521672
with client:
15531673
response = client.post(
@@ -1629,7 +1749,7 @@ def test_patch_relationship_missing_type(client, register_routes, computer, pers
16291749

16301750

16311751
def test_patch_relationship_missing_id(client, register_routes, computer, person):
1632-
payload = {"data": [{"type": "computer",}]}
1752+
payload = {"data": [{"type": "computer", }]}
16331753

16341754
with client:
16351755
response = client.patch(
@@ -1711,7 +1831,7 @@ def test_delete_relationship_missing_type(client, register_routes, computer, per
17111831

17121832

17131833
def test_delete_relationship_missing_id(client, register_routes, computer, person):
1714-
payload = {"data": [{"type": "computer",}]}
1834+
payload = {"data": [{"type": "computer", }]}
17151835

17161836
with client:
17171837
response = client.delete(

0 commit comments

Comments
 (0)