diff --git a/flask_rest_jsonapi/data_layers/alchemy.py b/flask_rest_jsonapi/data_layers/alchemy.py index e17e18a..dad9851 100644 --- a/flask_rest_jsonapi/data_layers/alchemy.py +++ b/flask_rest_jsonapi/data_layers/alchemy.py @@ -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" @@ -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) @@ -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) @@ -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)\ diff --git a/flask_rest_jsonapi/decorators.py b/flask_rest_jsonapi/decorators.py index b1c4ee5..a447811 100644 --- a/flask_rest_jsonapi/decorators.py +++ b/flask_rest_jsonapi/decorators.py @@ -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 diff --git a/flask_rest_jsonapi/resource.py b/flask_rest_jsonapi/resource.py index 1ad65ec..80221a6 100644 --- a/flask_rest_jsonapi/resource.py +++ b/flask_rest_jsonapi/resource.py @@ -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) @@ -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 """ @@ -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 """ @@ -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) @@ -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