Skip to content

Commit

Permalink
Merge branch 'enhancements' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott Wainstock committed May 28, 2014
2 parents 5c4b707 + 3497d3c commit db8fc6b
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 9 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ register((Artist, Album, Playlist))
```

And if you wanted to add custom logic for an endpoint? Or change the endpoint
name? Or add validation? All supported. Here's a "fancy" class definition:
name? Or change your top level json object name? Or add validation? All supported.
Here's a "fancy" class definition:

```python
class Style(Model):
Expand All @@ -172,6 +173,7 @@ class Style(Model):
__tablename__ = 'Genre'
__endpoint__ = 'styles'
__methods__ = ('GET', 'DELETE')
__top_level_json_name__ = 'Genres'

@staticmethod
def validate_GET(resource=None):
Expand Down
6 changes: 6 additions & 0 deletions sandman/model/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ class Model(object):
Default: None
"""

__top_level_json_name__ = None
"""The top level json text to output for this class
Default: 'resources'
"""

__methods__ = ('GET', 'POST', 'PATCH', 'DELETE', 'PUT')
"""override :attr:`__methods__` if you wish to change the HTTP methods
this :class:`sandman.model.Model` supports.
Expand Down
28 changes: 21 additions & 7 deletions sandman/sandman.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,20 +117,31 @@ def _single_resource_html_response(resource):
return make_response(render_template('resource.html', resource=resource,
tablename=tablename))

def _collection_json_response(resources, start, stop, depth=0):
def _collection_json_response(cls, resources, start, stop, depth=0):
"""Return the JSON representation of the collection *resources*.
:param list resources: list of :class:`sandman.model.Model`s to render
:rtype: :class:`flask.Response`
"""

top_level_json_name = None
if cls.__top_level_json_name__ is not None:
top_level_json_name = cls.__top_level_json_name__
else:
top_level_json_name = 'resources'

result_list = []
for resource in resources:
result_list.append(resource.as_dict(depth))

payload = {}
if start is not None:
return jsonify(resources=result_list[start:stop])
payload[top_level_json_name] = result_list[start:stop]
else:
return jsonify(resources=result_list)
payload[top_level_json_name] = result_list

return jsonify(payload)

def _collection_html_response(resources, start=0, stop=20):
"""Return the HTML representation of the collection *resources*.
Expand Down Expand Up @@ -211,16 +222,19 @@ def retrieve_collection(collection, query_arguments=None):
if query_arguments:
filters = []
order = []
limit = None
for key, value in query_arguments.items():
if key == 'page':
continue
if value.startswith('%'):
filters.append(getattr(cls, key).like(str(value), escape='/'))
elif key == 'sort':
order.append(getattr(cls, value))
elif key == 'limit':
limit = value
elif key:
filters.append(getattr(cls, key) == value)
resources = session.query(cls).filter(*filters).order_by(*order)
resources = session.query(cls).filter(*filters).order_by(*order).limit(limit)
else:
resources = session.query(cls).all()
return resources
Expand Down Expand Up @@ -259,7 +273,7 @@ def resource_created_response(resource):
resource.resource_uri())
return response

def collection_response(resources, start=None, stop=None):
def collection_response(cls, resources, start=None, stop=None):
"""Return a response for the *resources* of the appropriate content type.
:param resources: resources to be returned in request
Expand All @@ -268,7 +282,7 @@ def collection_response(resources, start=None, stop=None):
"""
if _get_acceptable_response_type() == JSON:
return _collection_json_response(resources, start, stop)
return _collection_json_response(cls, resources, start, stop)
else:
return _collection_html_response(resources, start, stop)

Expand Down Expand Up @@ -484,7 +498,7 @@ def get_collection(collection):
page = int(request.args['page'])
results_per_page = app.config.get('RESULTS_PER_PAGE', 20)
start, stop = page * results_per_page, (page +1) * results_per_page
return collection_response(resources, start, stop)
return collection_response(cls, resources, start, stop)

@app.route('/', methods=['GET'])
@etag
Expand Down
1 change: 1 addition & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class Album(Model):

__tablename__ = 'Album'
__methods__ = ('POST', 'PATCH', 'DELETE', 'PUT', 'GET')
__top_level_json_name__ = 'Albums'

def __str__(self):
"""Return string representation of *self*."""
Expand Down
13 changes: 12 additions & 1 deletion tests/test_sandman.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def setup_method(self, _):
self.app = app.test_client()
#pylint: disable=unused-variable
from . import models

def teardown_method(self, _):
"""Remove the database file copied during setup."""
os.unlink(self.DB_LOCATION)
Expand Down Expand Up @@ -72,6 +72,11 @@ def test_get(self):
response = self.get_response('/artists', 200)
assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 275

def test_get_with_limit(self):
"""Test simple HTTP GET"""
response = self.get_response('/artists', 200, params={'limit': 10})
assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 10

def test_get_with_filter(self):
"""Test simple HTTP GET"""
response = self.get_response('/artists', 200, params={'Name': 'AC/DC'})
Expand Down Expand Up @@ -284,6 +289,12 @@ def test_put_fail_validation(self):
'UnitPrice': 0.99,}))
assert response.status_code == 403

def test_responds_with_top_level_json_name_if_present(self):
"""Test top level json element is the one defined on the Model
rather than the string 'resources'"""
response = self.get_response('/albums', 200)
assert len(json.loads(response.get_data(as_text=True))[u'Albums']) == 347

class TestSandmanValidation(TestSandmanBase):
"""Sandman tests related to request validation"""

Expand Down

0 comments on commit db8fc6b

Please sign in to comment.