Skip to content

Commit 403a66a

Browse files
committed
Merge branch 'hotfix-0.11.4' into develop
2 parents 3dc8eb8 + 1a34946 commit 403a66a

File tree

9 files changed

+166
-39
lines changed

9 files changed

+166
-39
lines changed

Diff for: docs/conf.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,16 @@
128128
# further. For a list of options available for each theme, see the
129129
# documentation.
130130
#
131-
# html_theme_options = {}
131+
html_theme_options = {
132+
'github_user': 'miLibris',
133+
'github_repo': 'flask-rest-jsonapi',
134+
'github_banner': True,
135+
'travis_button': True,
136+
'show_related': True,
137+
'page_width': '1080px',
138+
'fixed_sidebar': True,
139+
'code_font_size': '0.8em'
140+
}
132141

133142
# Add any paths that contain custom themes here, relative to this directory.
134143
# html_theme_path = []

Diff for: docs/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Main concepts
99
-------------
1010

1111
.. image:: img/schema.png
12-
:width: 600px
12+
:width: 900px
1313
:alt: Architecture
1414

1515
| * `JSON API 1.0 specification <http://jsonapi.org/>`_: it is a very popular specification about client server interactions for REST JSON API. It helps you to work in team because it is very precise and sharable. Thanks to this specification your api offers lot of features like a strong structure of request and response, filtering, pagination, sparse fieldsets, including related objects, great error formatting etc.

Diff for: docs/resource_manager.rst

+12-14
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ Resource manager is the link between your logical data abstraction, your data la
99

1010
Flask-REST-JSONAPI provides 3 kinds of resource manager with default methods implementation according to JSONAPI 1.0 specification:
1111

12-
| * **ResourceList**: provides get and post methods to retrieve a collection of objects or create one.
13-
|
14-
| * **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update an object and delete an object
15-
|
16-
| * **ResourceRelationship**: provides get, post, patch and delete methods to get relationships, create relationships, update relationships and delete relationships between objects.
12+
* **ResourceList**: provides get and post methods to retrieve a collection of objects or create one.
13+
* **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update an object and delete an object
14+
* **ResourceRelationship**: provides get, post, patch and delete methods to get relationships, create relationships, update relationships and delete relationships between objects.
1715

1816
You can rewrite each default methods implementation to make custom work. If you rewrite all default methods implementation of a resource manager or if you rewrite a method and disable access to others, you don't have to set any attribute of your resource manager.
1917

@@ -49,17 +47,17 @@ All resource mangers are inherited from flask.views.MethodView so you can provid
4947

5048
You can plug additional decorators to each methods with this optional attributes:
5149

52-
:get_decorators: a list a decorators plugged to the get method
53-
:post_decorators: a list a decorators plugged to the post method
54-
:patch_decorators: a list a decorators plugged to the patch method
55-
:delete_decorators: a list a decorators plugged to the delete method
50+
* **get_decorators**: a list of decorators to plug to the get method
51+
* **post_decorators**: a list a decorators plugged to the post method
52+
* **patch_decorators**: a list a decorators plugged to the patch method
53+
* **delete_decorators**: a list a decorators plugged to the delete method
5654

57-
You can also provides default schema kwargs to each resource manager methods with this optional attributes:
55+
You can also provide default schema kwargs to each resource manager methods with this optional attributes:
5856

59-
:get_schema_kwargs: a dict of default schema kwargs in get method
60-
:post_schema_kwargs: a dict of default schema kwargs in post method
61-
:patch_schema_kwargs: a dict of default schema kwargs in patch method
62-
:delete_schema_kwargs: a dict of default schema kwargs in delete method
57+
* **get_schema_kwargs**: a dict of default schema kwargs in get method
58+
* **post_schema_kwargs**: a dict of default schema kwargs in post method
59+
* **patch_schema_kwargs**: a dict of default schema kwargs in patch method
60+
* **delete_schema_kwargs**: a dict of default schema kwargs in delete method
6361

6462
Each method of a resource manager got a pre and post process methods that take view args and kwargs as parameter for the pre process methods and the result of the method as parameter for the post process method. Thanks to this you can make custom work before and after the method process. Availables rewritable methods are:
6563

Diff for: flask_rest_jsonapi/data_layers/alchemy.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def get_object(self, view_kwargs):
6161
try:
6262
filter_field = getattr(self.model, id_field)
6363
except Exception:
64-
raise Exception("{} has no attribute {}".format(self.model.__name__), id_field)
64+
raise Exception("{} has no attribute {}".format(self.model.__name__, id_field))
6565

6666
url_field = getattr(self, 'url_field', 'id')
6767
filter_value = view_kwargs[url_field]

Diff for: flask_rest_jsonapi/data_layers/base.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ def __init__(self, kwargs):
3030
3131
:param dict kwargs: information about data layer instance
3232
"""
33+
if kwargs.get('methods') is not None:
34+
self.bound_additional_methods(kwargs['methods'])
35+
kwargs.pop('methods')
36+
3337
kwargs.pop('class', None)
3438

3539
for key, value in kwargs.items():
3640
setattr(self, key, value)
3741

38-
if kwargs.get('methods') is not None:
39-
self.bound_additional_methods()
40-
4142
def create_object(self, data, view_kwargs):
4243
"""Create an object
4344
@@ -310,10 +311,11 @@ def after_delete_relationship(self, obj, updated, json_data, relationship_field,
310311
"""
311312
raise NotImplementedError
312313

313-
def bound_additional_methods(self):
314+
def bound_additional_methods(self, methods):
314315
"""Bound additional methods to current instance
315316
316317
:param class meta: information from Meta class used to configure the data layer instance
317318
"""
318-
for key, value in self.methods.items():
319-
setattr(self, key, types.MethodType(value, self))
319+
for key, value in methods.items():
320+
if key in self.ADDITIONAL_METHODS:
321+
setattr(self, key, types.MethodType(value, self))

Diff for: flask_rest_jsonapi/data_layers/filtering/alchemy.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def __init__(self, model, filter_, resource, schema):
2929
self.schema = schema
3030

3131
def resolve(self):
32-
if 'or' not in self.filter_ and 'and' not in self.filter_:
32+
if 'or' not in self.filter_ and 'and' not in self.filter_ and 'not' not in self.filter_:
3333
if self.val is None and self.field is None:
3434
raise InvalidFilters("Can't find value or field in a filter")
3535

Diff for: flask_rest_jsonapi/resource.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ def __new__(cls):
3232
.format(cls.__name__))
3333

3434
data_layer_cls = cls.data_layer.get('class', SqlalchemyDataLayer)
35-
cls._data_layer = data_layer_cls(cls.data_layer)
35+
data_layer_kwargs = copy(cls.data_layer)
36+
cls._data_layer = data_layer_cls(data_layer_kwargs)
3637
cls._data_layer.resource = cls
3738

3839
for method in getattr(cls, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):

Diff for: setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from setuptools import setup, find_packages
22

33

4-
__version__ = '0.11.3'
4+
__version__ = '0.11.4'
55

66

77
setup(

Diff for: tests/test_sqlalchemy_data_layer.py

+130-13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from marshmallow_jsonapi import fields
1313

1414
from flask_rest_jsonapi import Api, ResourceList, ResourceDetail, ResourceRelationship, JsonApiException
15+
from flask_rest_jsonapi.exceptions import RelationNotFound, InvalidSort
1516
from flask_rest_jsonapi.querystring import QueryStringManager as QSManager
1617
from flask_rest_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
1718
from flask_rest_jsonapi.data_layers.base import BaseDataLayer
@@ -304,6 +305,21 @@ def register_routes(client, app, api_blueprint, person_list, person_detail, pers
304305
api.init_app(app)
305306

306307

308+
@pytest.fixture(scope="module")
309+
def get_object_mock():
310+
class get_object(object):
311+
foo = type('foo', (object,), {
312+
'property': type('prop', (object,), {
313+
'mapper': type('map', (object,), {
314+
'class_': 'test'
315+
})()
316+
})()
317+
})()
318+
def __init__(self, kwargs):
319+
pass
320+
return get_object
321+
322+
307323
# test good cases
308324
def test_get_list(client, register_routes, person, person_2):
309325
with client:
@@ -760,42 +776,105 @@ def test_get_list_field_error(client, register_routes):
760776

761777
def test_sqlalchemy_data_layer_without_session(person_model, person_list):
762778
with pytest.raises(Exception):
763-
SqlalchemyDataLayer(model=person_model, resource=person_list)
779+
SqlalchemyDataLayer(dict(model=person_model, resource=person_list))
764780

765781

766782
def test_sqlalchemy_data_layer_without_model(session, person_list):
767783
with pytest.raises(Exception):
768-
SqlalchemyDataLayer(session=session, resource=person_list)
784+
SqlalchemyDataLayer(dict(session=session, resource=person_list))
785+
786+
787+
def test_sqlalchemy_data_layer_create_object_error(session, person_model, person_list):
788+
with pytest.raises(JsonApiException):
789+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, resource=person_list))
790+
dl.create_object(dict(), dict())
769791

770792

771793
def test_sqlalchemy_data_layer_get_object_error(session, person_model):
772794
with pytest.raises(Exception):
773-
dl = SqlalchemyDataLayer(session=session, model=person_model, id_field='error')
774-
dl.get_object(**dict())
795+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, id_field='error'))
796+
dl.get_object(dict())
797+
798+
799+
def test_sqlalchemy_data_layer_update_object_error(session, person_model, person_list, monkeypatch):
800+
def commit_mock():
801+
raise JsonApiException()
802+
with pytest.raises(JsonApiException):
803+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, resource=person_list))
804+
monkeypatch.setattr(dl.session, 'commit', commit_mock)
805+
dl.update_object(dict(), dict(), dict())
806+
807+
808+
def test_sqlalchemy_data_layer_delete_object_error(session, person_model, person_list, monkeypatch):
809+
def commit_mock():
810+
raise JsonApiException()
811+
def delete_mock(obj):
812+
pass
813+
with pytest.raises(JsonApiException):
814+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, resource=person_list))
815+
monkeypatch.setattr(dl.session, 'commit', commit_mock)
816+
monkeypatch.setattr(dl.session, 'delete', delete_mock)
817+
dl.delete_object(dict(), dict())
775818

776819

777820
def test_sqlalchemy_data_layer_create_relationship_field_not_found(session, person_model):
778821
with pytest.raises(Exception):
779-
dl = SqlalchemyDataLayer(session=session, model=person_model)
780-
dl.create_relationship(dict(), 'error', '', **{'id': 1})
822+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
823+
dl.create_relationship(dict(), 'error', '', dict(id=1))
824+
825+
826+
def test_sqlalchemy_data_layer_create_relationship_error(session, person_model, get_object_mock, monkeypatch):
827+
def commit_mock():
828+
raise JsonApiException()
829+
with pytest.raises(JsonApiException):
830+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
831+
monkeypatch.setattr(dl.session, 'commit', commit_mock)
832+
monkeypatch.setattr(dl, 'get_object', get_object_mock)
833+
dl.create_relationship(dict(data=None), 'foo', '', dict(id=1))
781834

782835

783836
def test_sqlalchemy_data_layer_get_relationship_field_not_found(session, person_model):
784-
with pytest.raises(Exception):
785-
dl = SqlalchemyDataLayer(session=session, model=person_model)
786-
dl.get_relationship('error', '', '', **{'id': 1})
837+
with pytest.raises(RelationNotFound):
838+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
839+
dl.get_relationship('error', '', '', dict(id=1))
787840

788841

789842
def test_sqlalchemy_data_layer_update_relationship_field_not_found(session, person_model):
790843
with pytest.raises(Exception):
791-
dl = SqlalchemyDataLayer(session=session, model=person_model)
792-
dl.update_relationship(dict(), 'error', '', **{'id': 1})
844+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
845+
dl.update_relationship(dict(), 'error', '', dict(id=1))
846+
847+
848+
def test_sqlalchemy_data_layer_update_relationship_error(session, person_model, get_object_mock, monkeypatch):
849+
def commit_mock():
850+
raise JsonApiException()
851+
with pytest.raises(JsonApiException):
852+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
853+
monkeypatch.setattr(dl.session, 'commit', commit_mock)
854+
monkeypatch.setattr(dl, 'get_object', get_object_mock)
855+
dl.update_relationship(dict(data=None), 'foo', '', dict(id=1))
793856

794857

795858
def test_sqlalchemy_data_layer_delete_relationship_field_not_found(session, person_model):
796859
with pytest.raises(Exception):
797-
dl = SqlalchemyDataLayer(session=session, model=person_model)
798-
dl.delete_relationship(dict(), 'error', '', **{'id': 1})
860+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
861+
dl.delete_relationship(dict(), 'error', '', dict(id=1))
862+
863+
864+
def test_sqlalchemy_data_layer_delete_relationship_error(session, person_model, get_object_mock, monkeypatch):
865+
def commit_mock():
866+
raise JsonApiException()
867+
with pytest.raises(JsonApiException):
868+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
869+
monkeypatch.setattr(dl.session, 'commit', commit_mock)
870+
monkeypatch.setattr(dl, 'get_object', get_object_mock)
871+
dl.delete_relationship(dict(data=None), 'foo', '', dict(id=1))
872+
873+
874+
def test_sqlalchemy_data_layer_sort_query_error(session, person_model, monkeypatch):
875+
with pytest.raises(InvalidSort):
876+
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
877+
dl.sort_query(None, [dict(field='test')])
799878

800879

801880
def test_post_list_incorrect_type(client, register_routes, computer):
@@ -1277,6 +1356,44 @@ def test_base_data_layer():
12771356
base_dl.update_relationship(None, None, None, dict())
12781357
with pytest.raises(NotImplementedError):
12791358
base_dl.delete_relationship(None, None, None, dict())
1359+
with pytest.raises(NotImplementedError):
1360+
base_dl.query(dict())
1361+
with pytest.raises(NotImplementedError):
1362+
base_dl.before_create_object(None, dict())
1363+
with pytest.raises(NotImplementedError):
1364+
base_dl.after_create_object(None, None, dict())
1365+
with pytest.raises(NotImplementedError):
1366+
base_dl.before_get_object(dict())
1367+
with pytest.raises(NotImplementedError):
1368+
base_dl.after_get_object(None, dict())
1369+
with pytest.raises(NotImplementedError):
1370+
base_dl.before_get_collection(None, dict())
1371+
with pytest.raises(NotImplementedError):
1372+
base_dl.after_get_collection(None, None, dict())
1373+
with pytest.raises(NotImplementedError):
1374+
base_dl.before_update_object(None, None, dict())
1375+
with pytest.raises(NotImplementedError):
1376+
base_dl.after_update_object(None, None, dict())
1377+
with pytest.raises(NotImplementedError):
1378+
base_dl.before_delete_object(None, dict())
1379+
with pytest.raises(NotImplementedError):
1380+
base_dl.after_delete_object(None, dict())
1381+
with pytest.raises(NotImplementedError):
1382+
base_dl.before_create_relationship(None, None, None, dict())
1383+
with pytest.raises(NotImplementedError):
1384+
base_dl.after_create_relationship(None, None, None, None, None, dict())
1385+
with pytest.raises(NotImplementedError):
1386+
base_dl.before_get_relationship(None, None, None, dict())
1387+
with pytest.raises(NotImplementedError):
1388+
base_dl.after_get_relationship(None, None, None, None, None, dict())
1389+
with pytest.raises(NotImplementedError):
1390+
base_dl.before_update_relationship(None, None, None, dict())
1391+
with pytest.raises(NotImplementedError):
1392+
base_dl.after_update_relationship(None, None, None, None, None, dict())
1393+
with pytest.raises(NotImplementedError):
1394+
base_dl.before_delete_relationship(None, None, None, dict())
1395+
with pytest.raises(NotImplementedError):
1396+
base_dl.after_delete_relationship(None, None, None, None, None, dict())
12801397

12811398

12821399
def test_qs_manager():

0 commit comments

Comments
 (0)