Skip to content

Commit

Permalink
Merge branch 'hotfix-0.11.4' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
akira-dev committed Mar 22, 2017
2 parents 3dc8eb8 + 1a34946 commit 403a66a
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 39 deletions.
11 changes: 10 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,16 @@
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
html_theme_options = {
'github_user': 'miLibris',
'github_repo': 'flask-rest-jsonapi',
'github_banner': True,
'travis_button': True,
'show_related': True,
'page_width': '1080px',
'fixed_sidebar': True,
'code_font_size': '0.8em'
}

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Main concepts
-------------

.. image:: img/schema.png
:width: 600px
:width: 900px
:alt: Architecture

| * `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.
Expand Down
26 changes: 12 additions & 14 deletions docs/resource_manager.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ Resource manager is the link between your logical data abstraction, your data la

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

| * **ResourceList**: provides get and post methods to retrieve a collection of objects or create one.
|
| * **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update an object and delete an object
|
| * **ResourceRelationship**: provides get, post, patch and delete methods to get relationships, create relationships, update relationships and delete relationships between objects.
* **ResourceList**: provides get and post methods to retrieve a collection of objects or create one.
* **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update an object and delete an object
* **ResourceRelationship**: provides get, post, patch and delete methods to get relationships, create relationships, update relationships and delete relationships between objects.

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.

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

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

:get_decorators: a list a decorators plugged to the get method
:post_decorators: a list a decorators plugged to the post method
:patch_decorators: a list a decorators plugged to the patch method
:delete_decorators: a list a decorators plugged to the delete method
* **get_decorators**: a list of decorators to plug to the get method
* **post_decorators**: a list a decorators plugged to the post method
* **patch_decorators**: a list a decorators plugged to the patch method
* **delete_decorators**: a list a decorators plugged to the delete method

You can also provides default schema kwargs to each resource manager methods with this optional attributes:
You can also provide default schema kwargs to each resource manager methods with this optional attributes:

:get_schema_kwargs: a dict of default schema kwargs in get method
:post_schema_kwargs: a dict of default schema kwargs in post method
:patch_schema_kwargs: a dict of default schema kwargs in patch method
:delete_schema_kwargs: a dict of default schema kwargs in delete method
* **get_schema_kwargs**: a dict of default schema kwargs in get method
* **post_schema_kwargs**: a dict of default schema kwargs in post method
* **patch_schema_kwargs**: a dict of default schema kwargs in patch method
* **delete_schema_kwargs**: a dict of default schema kwargs in delete method

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:

Expand Down
2 changes: 1 addition & 1 deletion flask_rest_jsonapi/data_layers/alchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def get_object(self, view_kwargs):
try:
filter_field = getattr(self.model, id_field)
except Exception:
raise Exception("{} has no attribute {}".format(self.model.__name__), id_field)
raise Exception("{} has no attribute {}".format(self.model.__name__, id_field))

url_field = getattr(self, 'url_field', 'id')
filter_value = view_kwargs[url_field]
Expand Down
14 changes: 8 additions & 6 deletions flask_rest_jsonapi/data_layers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ def __init__(self, kwargs):
:param dict kwargs: information about data layer instance
"""
if kwargs.get('methods') is not None:
self.bound_additional_methods(kwargs['methods'])
kwargs.pop('methods')

kwargs.pop('class', None)

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

if kwargs.get('methods') is not None:
self.bound_additional_methods()

def create_object(self, data, view_kwargs):
"""Create an object
Expand Down Expand Up @@ -310,10 +311,11 @@ def after_delete_relationship(self, obj, updated, json_data, relationship_field,
"""
raise NotImplementedError

def bound_additional_methods(self):
def bound_additional_methods(self, methods):
"""Bound additional methods to current instance
:param class meta: information from Meta class used to configure the data layer instance
"""
for key, value in self.methods.items():
setattr(self, key, types.MethodType(value, self))
for key, value in methods.items():
if key in self.ADDITIONAL_METHODS:
setattr(self, key, types.MethodType(value, self))
2 changes: 1 addition & 1 deletion flask_rest_jsonapi/data_layers/filtering/alchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, model, filter_, resource, schema):
self.schema = schema

def resolve(self):
if 'or' not in self.filter_ and 'and' not in self.filter_:
if 'or' not in self.filter_ and 'and' not in self.filter_ and 'not' not in self.filter_:
if self.val is None and self.field is None:
raise InvalidFilters("Can't find value or field in a filter")

Expand Down
3 changes: 2 additions & 1 deletion flask_rest_jsonapi/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def __new__(cls):
.format(cls.__name__))

data_layer_cls = cls.data_layer.get('class', SqlalchemyDataLayer)
cls._data_layer = data_layer_cls(cls.data_layer)
data_layer_kwargs = copy(cls.data_layer)
cls._data_layer = data_layer_cls(data_layer_kwargs)
cls._data_layer.resource = cls

for method in getattr(cls, 'methods', ('GET', 'POST', 'PATCH', 'DELETE')):
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages


__version__ = '0.11.3'
__version__ = '0.11.4'


setup(
Expand Down
143 changes: 130 additions & 13 deletions tests/test_sqlalchemy_data_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from marshmallow_jsonapi import fields

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


@pytest.fixture(scope="module")
def get_object_mock():
class get_object(object):
foo = type('foo', (object,), {
'property': type('prop', (object,), {
'mapper': type('map', (object,), {
'class_': 'test'
})()
})()
})()
def __init__(self, kwargs):
pass
return get_object


# test good cases
def test_get_list(client, register_routes, person, person_2):
with client:
Expand Down Expand Up @@ -760,42 +776,105 @@ def test_get_list_field_error(client, register_routes):

def test_sqlalchemy_data_layer_without_session(person_model, person_list):
with pytest.raises(Exception):
SqlalchemyDataLayer(model=person_model, resource=person_list)
SqlalchemyDataLayer(dict(model=person_model, resource=person_list))


def test_sqlalchemy_data_layer_without_model(session, person_list):
with pytest.raises(Exception):
SqlalchemyDataLayer(session=session, resource=person_list)
SqlalchemyDataLayer(dict(session=session, resource=person_list))


def test_sqlalchemy_data_layer_create_object_error(session, person_model, person_list):
with pytest.raises(JsonApiException):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, resource=person_list))
dl.create_object(dict(), dict())


def test_sqlalchemy_data_layer_get_object_error(session, person_model):
with pytest.raises(Exception):
dl = SqlalchemyDataLayer(session=session, model=person_model, id_field='error')
dl.get_object(**dict())
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, id_field='error'))
dl.get_object(dict())


def test_sqlalchemy_data_layer_update_object_error(session, person_model, person_list, monkeypatch):
def commit_mock():
raise JsonApiException()
with pytest.raises(JsonApiException):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, resource=person_list))
monkeypatch.setattr(dl.session, 'commit', commit_mock)
dl.update_object(dict(), dict(), dict())


def test_sqlalchemy_data_layer_delete_object_error(session, person_model, person_list, monkeypatch):
def commit_mock():
raise JsonApiException()
def delete_mock(obj):
pass
with pytest.raises(JsonApiException):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model, resource=person_list))
monkeypatch.setattr(dl.session, 'commit', commit_mock)
monkeypatch.setattr(dl.session, 'delete', delete_mock)
dl.delete_object(dict(), dict())


def test_sqlalchemy_data_layer_create_relationship_field_not_found(session, person_model):
with pytest.raises(Exception):
dl = SqlalchemyDataLayer(session=session, model=person_model)
dl.create_relationship(dict(), 'error', '', **{'id': 1})
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
dl.create_relationship(dict(), 'error', '', dict(id=1))


def test_sqlalchemy_data_layer_create_relationship_error(session, person_model, get_object_mock, monkeypatch):
def commit_mock():
raise JsonApiException()
with pytest.raises(JsonApiException):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
monkeypatch.setattr(dl.session, 'commit', commit_mock)
monkeypatch.setattr(dl, 'get_object', get_object_mock)
dl.create_relationship(dict(data=None), 'foo', '', dict(id=1))


def test_sqlalchemy_data_layer_get_relationship_field_not_found(session, person_model):
with pytest.raises(Exception):
dl = SqlalchemyDataLayer(session=session, model=person_model)
dl.get_relationship('error', '', '', **{'id': 1})
with pytest.raises(RelationNotFound):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
dl.get_relationship('error', '', '', dict(id=1))


def test_sqlalchemy_data_layer_update_relationship_field_not_found(session, person_model):
with pytest.raises(Exception):
dl = SqlalchemyDataLayer(session=session, model=person_model)
dl.update_relationship(dict(), 'error', '', **{'id': 1})
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
dl.update_relationship(dict(), 'error', '', dict(id=1))


def test_sqlalchemy_data_layer_update_relationship_error(session, person_model, get_object_mock, monkeypatch):
def commit_mock():
raise JsonApiException()
with pytest.raises(JsonApiException):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
monkeypatch.setattr(dl.session, 'commit', commit_mock)
monkeypatch.setattr(dl, 'get_object', get_object_mock)
dl.update_relationship(dict(data=None), 'foo', '', dict(id=1))


def test_sqlalchemy_data_layer_delete_relationship_field_not_found(session, person_model):
with pytest.raises(Exception):
dl = SqlalchemyDataLayer(session=session, model=person_model)
dl.delete_relationship(dict(), 'error', '', **{'id': 1})
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
dl.delete_relationship(dict(), 'error', '', dict(id=1))


def test_sqlalchemy_data_layer_delete_relationship_error(session, person_model, get_object_mock, monkeypatch):
def commit_mock():
raise JsonApiException()
with pytest.raises(JsonApiException):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
monkeypatch.setattr(dl.session, 'commit', commit_mock)
monkeypatch.setattr(dl, 'get_object', get_object_mock)
dl.delete_relationship(dict(data=None), 'foo', '', dict(id=1))


def test_sqlalchemy_data_layer_sort_query_error(session, person_model, monkeypatch):
with pytest.raises(InvalidSort):
dl = SqlalchemyDataLayer(dict(session=session, model=person_model))
dl.sort_query(None, [dict(field='test')])


def test_post_list_incorrect_type(client, register_routes, computer):
Expand Down Expand Up @@ -1277,6 +1356,44 @@ def test_base_data_layer():
base_dl.update_relationship(None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.delete_relationship(None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.query(dict())
with pytest.raises(NotImplementedError):
base_dl.before_create_object(None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_create_object(None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_get_object(dict())
with pytest.raises(NotImplementedError):
base_dl.after_get_object(None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_get_collection(None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_get_collection(None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_update_object(None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_update_object(None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_delete_object(None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_delete_object(None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_create_relationship(None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_create_relationship(None, None, None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_get_relationship(None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_get_relationship(None, None, None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_update_relationship(None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_update_relationship(None, None, None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.before_delete_relationship(None, None, None, dict())
with pytest.raises(NotImplementedError):
base_dl.after_delete_relationship(None, None, None, None, None, dict())


def test_qs_manager():
Expand Down

0 comments on commit 403a66a

Please sign in to comment.