Skip to content

Commit

Permalink
relationship management done
Browse files Browse the repository at this point in the history
  • Loading branch information
akira-dev committed Jan 17, 2017
1 parent c7c850b commit 841a4d6
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 23 deletions.
56 changes: 54 additions & 2 deletions flask_rest_jsonapi/data_layers/alchemy.py
Expand Up @@ -51,7 +51,7 @@ def get_items(self, qs, **view_kwargs):
:param QueryStringManager qs: a querystring manager to retrieve information from url
:param dict view_kwargs: kwargs from the resource view
:return int item_count: the number of items in the collection
:return list query.all(): the list of items
:return tuple: the number of item and the list of items
"""
if not hasattr(self, 'get_base_query'):
raise Exception("You must provide an get_base_query in data layer kwargs in %s"
Expand Down Expand Up @@ -116,7 +116,10 @@ def delete_item(self, item, **view_kwargs):
def get_relationship(self, related_resource_type, related_id_field, **view_kwargs):
"""Get related data of a relationship
:param
:param str related_resource_type: the related resource type name
:param str related_id_field: the identifier field of the related model
:param dict view_kwargs: kwargs from the resource view
:return tuple: the item and related resource(s)
"""
item = self.get_item(**view_kwargs)

Expand All @@ -136,6 +139,10 @@ def get_relationship(self, related_resource_type, related_id_field, **view_kwarg

def update_relationship(self, json_data, related_id_field, **view_kwargs):
"""Update a relationship
:param dict json_data: the request params
:param str related_id_field: the identifier field of the related model
:param dict view_kwargs: kwargs from the resource view
"""
item = self.get_item(**view_kwargs)

Expand All @@ -161,8 +168,53 @@ def update_relationship(self, json_data, related_id_field, **view_kwargs):

self.session.commit()

def add_relationship(self, json_data, related_id_field, **view_kwargs):
"""Add / create a relationship
:param dict json_data: the request params
:param str related_id_field: the identifier field of the related model
:param dict view_kwargs: kwargs from the resource view
"""
item = self.get_item(**view_kwargs)

if not hasattr(item, self.relationship_attribut):
raise RelationNotFound

related_model = getattr(item.__class__, self.relationship_attribut).property.mapper.class_

for item_ in json_data['data']:
related_item = self.get_related_item(related_model, related_id_field, item_)
getattr(item, self.relationship_attribut).append(related_item)

self.session.commit()

def remove_relationship(self, json_data, related_id_field, **view_kwargs):
"""Remove a relationship
:param dict json_data: the request params
:param str related_id_field: the identifier field of the related model
:param dict view_kwargs: kwargs from the resource view
"""
item = self.get_item(**view_kwargs)

if not hasattr(item, self.relationship_attribut):
raise RelationNotFound

related_model = getattr(item.__class__, self.relationship_attribut).property.mapper.class_

for item_ in json_data['data']:
related_item = self.get_related_item(related_model, related_id_field, item_)
getattr(item, self.relationship_attribut).remove(related_item)

self.session.commit()

def get_related_item(self, related_model, related_id_field, item):
"""Get a related item
:param Model related_model: an sqlalchemy model
:param str related_id_field: the identifier field of the related model
:param DeclarativeMeta item: the sqlalchemy item instance to retrieve related items from
:return DeclarativeMeta: a related item
"""
try:
related_item = self.session.query(related_model)\
Expand Down
50 changes: 32 additions & 18 deletions flask_rest_jsonapi/decorators.py
Expand Up @@ -37,33 +37,47 @@ def check_requirements(f):
"""
"""
def wrapped_f(self, *args, **kwargs):
cls = type(self)
cls_name = type(self).__name__
method_name = f.__name__.upper()

error_message = "You must provide %(error_field)s in %(cls)s to get access to the default %(method)s method"
error_message_data = {'cls': cls_name, 'method': method_name}

if not hasattr(self, 'data_layer'):
raise Exception("You must provide data layer information in %s to get access to the default %s \
method" % (cls.__name__, f.__name__.upper()))
if 'ResourceList' in [cls_.__name__ for cls_ in cls.__bases__]:
raise Exception(error_message % error_message_data.update({'error_field': 'data layer information'}))

if 'ResourceList' in [cls_name for cls_ in type(self).__bases__]:
if not hasattr(self, 'schema') or not isinstance(self.schema, dict) \
or self.schema.get('cls') is None:
raise Exception("You must provide schema information in %s to get access to the default %s method"
% (cls.__name__, f.__name__.upper()))
if f.__name__.upper() == 'GET':
raise Exception(error_message % error_message_data.update({'error_field': 'schema information'}))
if method_name == 'GET':
if not hasattr(self, 'resource_type'):
raise Exception("You must provide resource type in %s to get access to the default %s method"
% (cls.__name__, f.__name__.upper()))
raise Exception(error_message % error_message_data.update({'error_field': 'resource_type'}))
if not hasattr(self, 'endpoint') or not isinstance(self.endpoint, dict) \
or self.endpoint.get('name') is None:
raise Exception("You must provide endpoint information in %s to get access to the default %s method"
% (cls.__name__, f.__name__.upper()))
if 'ResourceDetail' in [cls_.__name__ for cls_ in cls.__bases__]:
if f.__name__.upper() in ('GET', 'PATCH'):
raise Exception(error_message % error_message_data.update({'error_field': 'endpoint infromation'}))

if 'ResourceDetail' in [cls_name for cls_ in type(self).__bases__]:
if method_name in ('GET', 'PATCH'):
if not hasattr(self, 'schema') or not isinstance(self.schema, dict) \
or self.schema.get('cls') is None:
raise Exception("You must provide schema information in %s to get access to the default %s method"
% (cls.__name__, f.__name__.upper()))
if f.__name__.upper() == 'GET':
raise Exception(error_message % error_message_data.update({'error_field': 'schema information'}))
if method_name == 'GET':
if not hasattr(self, 'resource_type'):
raise Exception("You must provide resource type in %s to get access to the default %s method"
% (cls.__name__, f.__name__.upper()))
raise Exception(error_message % error_message_data.update({'error_field': 'resource_type'}))

if 'ResourceRelationship' in [cls_name for cls_ in type(self).__bases__]:
if method_name in ('GET', 'POST', 'PATCH', 'DELETE'):
if not hasattr(self, 'related_resource_type'):
raise Exception(error_message % error_message_data.update({'error_field': 'related_resource_type'}))
if not hasattr(self, 'related_id_field'):
raise Exception(error_message % error_message_data.update({'error_field': 'related_id_field'}))
if method_name == 'GET':
if not hasattr(self, 'endpoint'):
raise Exception(error_message % error_message_data.update({'error_field': 'endpoint'}))
if not hasattr(self, 'related_endpoint'):
raise Exception(error_message % error_message_data.update({'error_field': 'related_endpoint'}))

return f(self, *args, **kwargs)

return wrapped_f
84 changes: 81 additions & 3 deletions flask_rest_jsonapi/resource.py
Expand Up @@ -85,6 +85,31 @@ def __init__(cls, name, bases, nmspc):
cls.delete = delete_decorator(cls.delete)


class ResourceRelationshipMeta(ResourceMeta):

def __init__(cls, name, bases, nmspc):
super(ResourceRelationshipMeta, cls).__init__(name, bases, nmspc)
meta = nmspc.get('Meta')

if meta is not None:
get_decorators = getattr(meta, 'get_decorators', [])
post_decorators = getattr(meta, 'post_decorators', [])
patch_decorators = getattr(meta, 'patch_decorators', [])
delete_decorators = getattr(meta, 'delete_decorators', [])

for get_decorator in get_decorators:
cls.get = get_decorator(cls.get)

for post_decorator in post_decorators:
cls.post = post_decorator(cls.post)

for patch_decorator in patch_decorators:
cls.patch = patch_decorator(cls.patch)

for delete_decorator in delete_decorators:
cls.delete = delete_decorator(cls.delete)


class Resource(MethodView):

decorators = (check_headers, add_headers)
Expand Down Expand Up @@ -259,8 +284,9 @@ def delete(self, *args, **kwargs):
return '', 204


class Relationship(with_metaclass(ResourceMeta, Resource)):
class Relationship(with_metaclass(ResourceRelationshipMeta, Resource)):

@check_requirements
def get(self, *args, **kwargs):
"""Get a relationship details
"""
Expand All @@ -287,6 +313,32 @@ def get(self, *args, **kwargs):
'related': url_for(self.related_endpoint, **related_endpoint_kwargs)},
'data': data}

@check_requirements
def post(self, *args, **kwargs):
"""Add / create relationship(s)
"""
json_data = request.get_json()

if 'data' not in json_data or not isinstance(json_data.get('data'), list):
return ErrorFormatter.format_error(["You must provide a dictionary with a data key in params"]), 400

for item in json_data['data']:
if 'type' not in item or 'id' not in item:
return ErrorFormatter.format_error(["You must provide a type and an id in data params"]), 400
if item['type'] != self.related_resource_type:
return ErrorFormatter.format_error(["The resource type provided in params does not match the resource \
type declared in the relationship resource"]), 400

try:
self.data_layer.add_relationship(json_data, self.related_id_field, **kwargs)
except RelationNotFound:
return ErrorFormatter.format_error(["Relationship %s not found on model %s"
% (self.data_layer.relationship_attribut,
self.data_layer.model.__name__)]), 404
except EntityNotFound as e:
return ErrorFormatter.format_error([e.message]), e.status_code

@check_requirements
def patch(self, *args, **kwargs):
"""Update a relationship
"""
Expand All @@ -305,6 +357,9 @@ def patch(self, *args, **kwargs):
for item in json_data['data']:
if 'type' not in item or 'id' not in item:
return ErrorFormatter.format_error(["You must provide a type and an id in data params"]), 400
if item['type'] != self.related_resource_type:
return ErrorFormatter.format_error(["The resource type provided in params does not match the resource \
type declared in the relationship resource"]), 400

try:
self.data_layer.update_relationship(json_data, self.related_id_field, **kwargs)
Expand All @@ -314,7 +369,30 @@ def patch(self, *args, **kwargs):
self.data_layer.model.__name__)]), 404
except EntityNotFound as e:
return ErrorFormatter.format_error([e.message]), e.status_code
# except Exception as e:
# return ErrorFormatter.format_error([str(e)]), 500

return ''

@check_requirements
def delete(self, *args, **kwargs):
"""Delete relationship(s)
"""
json_data = request.get_json()

if 'data' not in json_data or not isinstance(json_data.get('data'), list):
return ErrorFormatter.format_error(["You must provide a dictionary with a data key in params"]), 400

for item in json_data['data']:
if 'type' not in item or 'id' not in item:
return ErrorFormatter.format_error(["You must provide a type and an id in data params"]), 400
if item['type'] != self.related_resource_type:
return ErrorFormatter.format_error(["The resource type provided in params does not match the resource \
type declared in the relationship resource"]), 400

try:
self.data_layer.remove_relationship(json_data, self.related_id_field, **kwargs)
except RelationNotFound:
return ErrorFormatter.format_error(["Relationship %s not found on model %s"
% (self.data_layer.relationship_attribut,
self.data_layer.model.__name__)]), 404
except EntityNotFound as e:
return ErrorFormatter.format_error([e.message]), e.status_code

0 comments on commit 841a4d6

Please sign in to comment.