Skip to content

Commit

Permalink
Support assoc. proxies in relationship endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
jfinkels committed Feb 28, 2017
1 parent 1129d46 commit e5ec92f
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 39 deletions.
31 changes: 30 additions & 1 deletion flask_restless/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ def session_query(session, model):
return session.query(model)


# TODO Combine this function with the one below.
def scalar_collection_proxied_relations(model):
"""Yields the name of each relationship proxied to a scalar collection.
This includes each relationship to an association table for which
there is an association proxy that presents a scalar collection (for
example, a list of strings).
.. seealso::
:func:`assoc_proxy_scalar_collections`
Yields the names of association proxies for the relationships
found by this function.
.. versionadded:: 1.0.0
"""
mapper = sqlalchemy_inspect(model)
for k, v in mapper.all_orm_descriptors.items():
if isinstance(v, AssociationProxy):
# HACK SQLAlchemy only loads the association proxy
# on-demand. We need to call `hasattr` in order to force
# SQLAlchemy to load the attribute.
hasattr(model, k)
if not isinstance(v.remote_attr.property, RelationshipProperty):
if is_like_list(model, v.local_attr.key):
yield v.local_attr.key


def assoc_proxy_scalar_collections(model):
"""Yields the name of each association proxy collection as a string.
Expand All @@ -76,7 +105,7 @@ def assoc_proxy_scalar_collections(model):
.. seealso::
:func:`assoc_proxy_inst_collections`
:func:`scalar_collection_proxied_relations`
.. versionadded:: 1.0.0
Expand Down
25 changes: 25 additions & 0 deletions flask_restless/views/relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from flask import request
from werkzeug.exceptions import BadRequest

from ..helpers import scalar_collection_proxied_relations
from ..helpers import collection_name
from ..helpers import get_by
from ..helpers import get_related_model
Expand Down Expand Up @@ -93,6 +94,14 @@ def get(self, resource_id, relation_name):
if primary_resource is None:
detail = 'No resource with ID {0}'.format(resource_id)
return error_response(404, detail=detail)
# Check if the relation is to an association table through which
# there is an association proxy that proxies to a scalar
# collection. In this situation, we wish to hide the
# relationship entirely, since the scalar collection is exposed
# as an attribute.
if relation_name in scalar_collection_proxied_relations(self.model):
detail = 'No relationship named {0}'.format(relation_name)
return error_response(404, detail=detail)
if is_like_list(primary_resource, relation_name):
try:
filters, sort, group_by, single = \
Expand Down Expand Up @@ -225,6 +234,14 @@ def patch(self, resource_id, relation_name):
detail = 'Model {0} has no relation named {1}'
detail = detail.format(self.model, relation_name)
return error_response(404, detail=detail)
# Check if the relation is to an association table through which
# there is an association proxy that proxies to a scalar
# collection. In this situation, we wish to hide the
# relationship entirely, since the scalar collection is exposed
# as an attribute.
if relation_name in scalar_collection_proxied_relations(self.model):
detail = 'No relationship named {0}'.format(relation_name)
return error_response(404, detail=detail)
related_model = get_related_model(self.model, relation_name)
# related_value = getattr(instance, relation_name)

Expand Down Expand Up @@ -342,6 +359,14 @@ def delete(self, resource_id, relation_name):
resource_id = temp_result
instance = get_by(self.session, self.model, resource_id,
self.primary_key)
# Check if the relation is to an association table through which
# there is an association proxy that proxies to a scalar
# collection. In this situation, we wish to hide the
# relationship entirely, since the scalar collection is exposed
# as an attribute.
if relation_name in scalar_collection_proxied_relations(self.model):
detail = 'No relationship named {0}'.format(relation_name)
return error_response(404, detail=detail)
# If no such relation exists, return an error to the client.
if not hasattr(instance, relation_name):
detail = 'No such link: {0}'.format(relation_name)
Expand Down
65 changes: 27 additions & 38 deletions tests/test_associationproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,15 +293,15 @@ def test_fetching(self):
self.manager.create_api(self.Article)

article = self.Article(id=1)
article.tag_names = ['foo', 'bar']
article.tag_names = [u'foo', u'bar']
self.session.add(article)
self.session.commit()

response = self.app.get('/api/article/1')
document = loads(response.data)
article = document['data']
tag_names = sorted(article['attributes']['tag_names'])
self.assertEqual(tag_names, ['bar', 'foo'])
self.assertEqual(tag_names, [u'bar', u'foo'])

def test_creating(self):
"""Tests for creating with an association proxy to a scalar list."""
Expand All @@ -311,7 +311,7 @@ def test_creating(self):
'data': {
'type': 'article',
'attributes': {
'tag_names': ['foo', 'bar']
'tag_names': [u'foo', u'bar']
}
}
}
Expand All @@ -321,19 +321,19 @@ def test_creating(self):
# Check that the response includes the `tag_names` attribute.
document = loads(response.data)
article = document['data']
self.assertEqual(article['attributes']['tag_names'], ['foo', 'bar'])
self.assertEqual(article['attributes']['tag_names'], [u'foo', u'bar'])

# Check that the Article object has been created and has the tag names.
self.assertEqual(self.session.query(self.Article).count(), 1)
article = self.session.query(self.Article).first()
self.assertEqual(article.tag_names, ['foo', 'bar'])
self.assertEqual(article.tag_names, [u'foo', u'bar'])

def test_updating(self):
"""Tests for updating an association proxy to a scalar list."""
self.manager.create_api(self.Article, methods=['PATCH'])

article = self.Article(id=1)
article.tag_names = ['foo', 'bar']
article.tag_names = [u'foo', u'bar']
self.session.add(article)
self.session.commit()

Expand All @@ -342,20 +342,20 @@ def test_updating(self):
'type': 'article',
'id': '1',
'attributes': {
'tag_names': ['baz', 'xyzzy']
'tag_names': [u'baz', u'xyzzy']
}
}
}
response = self.app.patch('/api/article/1', data=dumps(data))
self.assertEqual(response.status_code, 204)
self.assertEqual(article.tag_names, ['baz', 'xyzzy'])
self.assertEqual(article.tag_names, [u'baz', u'xyzzy'])

def test_deleting(self):
"""Test for deleting a resource with a to-many relationship."""
self.manager.create_api(self.Article, methods=['DELETE'])

article = self.Article(id=1)
article.tag_names = ['foo', 'bar']
article.tag_names = [u'foo', u'bar']
self.session.add(article)
self.session.commit()

Expand All @@ -368,24 +368,22 @@ def test_deleting(self):
response = self.app.delete('/api/article/1', data=dumps(data))
self.assertEqual(response.status_code, 204)
tags = self.session.query(self.Tag).all()
self.assertEqual(['bar', 'foo'], sorted(tag.name for tag in tags))
self.assertEqual([u'bar', u'foo'], sorted(tag.name for tag in tags))

def test_fetch_relationships(self):
"""Test for fetching to-many relationship resource identifiers."""
self.manager.create_api(self.Article)
self.manager.create_api(self.Tag)

article = self.Article(id=1)
article.tag_names = ['foo', 'bar']
article.tag_names = [u'foo', u'bar']
self.session.add(article)
self.session.commit()

# TODO What to do about this situation? The `tags` relationship
# is not shown in the resource representation of the Article
# object because we assume the `tag_names` attribute is the only
# thing the user wants to expose. However, the Tag objects
# underlying the `tag_names` are visible in the relationships
# attribute.
# The `tags` relationship is not shown in the resource
# representation of the Article object because we assume the
# `tag_names` attribute is the only thing the user wants to
# expose.
response = self.app.get('/api/article/1/relationships/tags')
self.assertEqual(response.status_code, 404)

Expand All @@ -398,13 +396,10 @@ def test_adding_to_relationship(self):
self.session.add(article)
self.session.commit()

# TODO What to do about this situation? The `tags` relationship
# is not shown in the resource representation of the Article
# object because we assume the `tag_names` attribute is the only
# thing the user wants to expose. However, the Tag objects
# underlying the `tag_names` are visible in the relationships
# attribute. Maybe we shouldn't actually hide the `tags`
# relationship.
# The `tags` relationship is not shown in the resource
# representation of the Article object because we assume the
# `tag_names` attribute is the only thing the user wants to
# expose.
data = {
'data': [
{'type': 'tag', 'id': '1'},
Expand All @@ -427,13 +422,10 @@ def test_removing_from_relationship(self):
self.session.add_all([article, tag])
self.session.commit()

# TODO What to do about this situation? The `tags` relationship
# is not shown in the resource representation of the Article
# object because we assume the `tag_names` attribute is the only
# thing the user wants to expose. However, the Tag objects
# underlying the `tag_names` are visible in the relationships
# attribute. Maybe we shouldn't actually hide the `tags`
# relationship.
# The `tags` relationship is not shown in the resource
# representation of the Article object because we assume the
# `tag_names` attribute is the only thing the user wants to
# expose.
data = {
'data': [
{'type': 'tag', 'id': '1'},
Expand All @@ -456,13 +448,10 @@ def test_replacing_relationship(self):
self.session.add_all([article, tag1, tag2])
self.session.commit()

# TODO What to do about this situation? The `tags` relationship
# is not shown in the resource representation of the Article
# object because we assume the `tag_names` attribute is the only
# thing the user wants to expose. However, the Tag objects
# underlying the `tag_names` are visible in the relationships
# attribute. Maybe we shouldn't actually hide the `tags`
# relationship.
# The `tags` relationship is not shown in the resource
# representation of the Article object because we assume the
# `tag_names` attribute is the only thing the user wants to
# expose.
data = {
'data': [
{'type': 'tag', 'id': '2'},
Expand Down

0 comments on commit e5ec92f

Please sign in to comment.