diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..cf98d74 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[run] +branch=True +source= + marshmallow_jsonapi + tests diff --git a/marshmallow_jsonapi/flask.py b/marshmallow_jsonapi/flask.py index e805e11..d2a33d0 100644 --- a/marshmallow_jsonapi/flask.py +++ b/marshmallow_jsonapi/flask.py @@ -60,6 +60,41 @@ class Meta: """ pass + def __init__(self, *args, **kwargs): + if kwargs.get('self_url', None): + raise TypeError('Use `self_view` instead of `self_url` ' + 'using the Flask extension.') + if kwargs.get('self_url_kwargs', None): + raise TypeError('Use `self_view_kwargs` instead of ' + '`self_url_kwargs` when using the Flask extension.') + if kwargs.get('related_url', None): + raise TypeError('Use `related_view` instead of `related_url` ' + 'using the Flask extension.') + if kwargs.get('related_url_kwargs', None): + raise TypeError('Use `related_view_kwargs` instead of ' + '`related_url_kwargs` when using the Flask extension.') + + self_view = kwargs.pop('self_view', None) + self_view_kwargs = kwargs.pop('self_view_kwargs', dict()) + related_view = kwargs.pop('related_view', None) + related_view_kwargs = kwargs.pop('related_view_kwargs', dict()) + + if self_view_kwargs and not self_view: + raise ValueError('Must specify `self_view` keyword when ' + '`self_view_kwargs` is specified.') + + if related_view_kwargs and not related_view: + raise ValueError('Must specify `related_view` keyword when ' + '`related_view_kwargs` is specified.') + + if self_view: + kwargs['self_url'] = flask.url_for(self_view, **self_view_kwargs) + if related_view: + kwargs['related_url'] = flask.url_for(related_view, + **related_view_kwargs) + + super(Schema, self).__init__(*args, **kwargs) + def generate_url(self, view_name, **kwargs): """Generate URL with any kwargs interpolated.""" return flask.url_for(view_name, **kwargs) if view_name else None diff --git a/marshmallow_jsonapi/schema.py b/marshmallow_jsonapi/schema.py index af07a3e..ef6e88c 100644 --- a/marshmallow_jsonapi/schema.py +++ b/marshmallow_jsonapi/schema.py @@ -85,6 +85,9 @@ class Meta: def __init__(self, *args, **kwargs): self.include_data = kwargs.pop('include_data', ()) + self.resource_identifier = kwargs.pop('resource_identifier', False) + self.override_self_url = kwargs.pop('self_url', None) + self.override_related_url = kwargs.pop('related_url', None) super(Schema, self).__init__(*args, **kwargs) if self.include_data: self.check_relations(self.include_data) @@ -364,13 +367,19 @@ def format_item(self, item): ret['relationships'] = self.dict_class() ret['relationships'][self.inflect(field_name)] = value else: + # Resource identifier objects don't get attributes. + if self.resource_identifier: + continue + if 'attributes' not in ret: ret['attributes'] = self.dict_class() ret['attributes'][self.inflect(field_name)] = value - links = self.get_resource_links(item) - if links: - ret['links'] = links + # Resource identifier objects don't get links + if not self.resource_identifier: + links = self.get_resource_links(item) + if links: + ret['links'] = links return ret def format_items(self, data, many): @@ -391,10 +400,21 @@ def get_top_level_links(self, data, many): if self.opts.self_url_many: self_link = self.generate_url(self.opts.self_url_many) else: - if self.opts.self_url: + if self.opts.self_url and data is not None: self_link = data.get('links', {}).get('self', None) - return {'self': self_link} + if self.override_self_url is not None: + self_link = self.override_self_url + + links = dict() + + if self_link is not None: + links['self'] = self_link + + if self.override_related_url: + links['related'] = self.override_related_url + + return links def get_resource_links(self, item): """Hook for adding links to a resource object.""" @@ -412,7 +432,7 @@ def wrap_response(self, data, many): # may only be included if there is data in the ret if many or data: top_level_links = self.get_top_level_links(data, many) - if top_level_links['self']: + if top_level_links: ret['links'] = top_level_links return ret diff --git a/tests/base.py b/tests/base.py index be5eef9..f26a3f8 100644 --- a/tests/base.py +++ b/tests/base.py @@ -48,7 +48,12 @@ def get_top_level_links(self, data, many): self_link = '/authors/' else: self_link = '/authors/{}'.format(data['id']) - return {'self': self_link} + links = {'self': self_link} + + schema_links = super(AuthorSchema, self).get_top_level_links(data, many) + links.update(schema_links) + + return links class Meta: type_ = 'people' diff --git a/tests/test_flask.py b/tests/test_flask.py index 49eaff7..3019f20 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -99,6 +99,37 @@ def test_self_link_many(self, app, posts): assert 'links' in data['data'][0] assert data['data'][0]['links']['self'] == '/posts/{}/'.format(posts[0].id) + @pytest.mark.parametrize('kwargs, exc, msg', [ + (dict(self_url='foo'), TypeError, 'self_view'), + (dict(self_url_kwargs='foo'), TypeError, 'self_view_kwargs'), + (dict(related_url='foo'), TypeError, 'related_view'), + (dict(related_url_kwargs='foo'), TypeError, 'related_view_kwargs'), + (dict(self_view_kwargs='foo'), ValueError, 'self_view'), + (dict(related_view_kwargs='foo'), ValueError, 'related_view'), + ]) + def test_schema_view_override_exceptions(self, kwargs, exc, msg): + with pytest.raises(exc) as exc_info: + self.PostFlaskSchema(**kwargs) + + assert msg in exc_info.value.args[0] + + def test_view_override(self, app): + # Here we use "wrong" views just to check output. + + schema = self.PostFlaskSchema(self_view='posts') + assert schema.override_self_url == '/posts/' + + schema = self.PostFlaskSchema( + self_view='author_detail', self_view_kwargs={'author_id': 1}) + assert schema.override_self_url == '/authors/1' + + schema = self.PostFlaskSchema(related_view='posts') + assert schema.override_related_url == '/posts/' + + schema = self.PostFlaskSchema( + related_view='author_detail', related_view_kwargs={'author_id': 1}) + assert schema.override_related_url == '/authors/1' + def test_schema_with_empty_relationship(self, app, post_with_null_author): data = unpack(self.PostAuthorFlaskSchema().dump(post_with_null_author)) assert 'relationships' not in data diff --git a/tests/test_schema.py b/tests/test_schema.py index 9ce7c5f..4030bc3 100755 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -112,6 +112,34 @@ def test_dump_empty_list(self): assert 'links' in data assert data['links']['self'] == '/authors/' + def test_dump_single_resource_identifer(self, author): + data = unpack(AuthorSchema(resource_identifier=True).dump(author)) + + assert data == { + 'data': {'id': str(author.id), 'type': 'people'}, + 'links': {'self': '/authors/' + str(author.id)} + } + + def test_dump_many_resource_identifer(self, authors): + schema = AuthorSchema(many=True, resource_identifier=True) + data = unpack(schema.dump(authors)) + + assert set(data.keys()) == {'data', 'links'} + assert data['links'] == {'self': '/authors/'} + for i, author in enumerate(authors): + assert data['data'][i] == {'id': str(authors[i].id), 'type': 'people'} + + def test_dump_none_resource_identifier(self): + data = unpack(AuthorSchema(resource_identifier=True).dump(None)) + + assert 'data' in data + assert data['data'] is None + assert 'links' not in data + + def test_dump_empty_list_resource_identifier(self): + data = unpack(AuthorSchema(many=True, resource_identifier=True).dump([])) + assert data == {'data': [], 'links': {'self': '/authors/'}} + class TestCompoundDocuments: @@ -724,3 +752,24 @@ class Meta: assert assert_relationship_error('author', errors['errors']) assert assert_relationship_error('comments', errors['errors']) + + +class TestOverrideUrls(object): + def test_self_url(self, author): + url = '/articles/1/author' + schema = AuthorSchema(self_url=url) + data = unpack(schema.dump(author)) + + assert data['links']['self'] == url + + def test_related_url(self, author): + self_url = '/articles/1/relationships/author' + related_url = '/articles/1/author' + schema = AuthorSchema( + resource_identifier=True, + self_url=self_url, + related_url=related_url) + data = unpack(schema.dump(author)) + + assert data['links']['self'] == self_url + assert data['links']['related'] == related_url