Skip to content

Commit

Permalink
Optional dictification of objects in JSON responses
Browse files Browse the repository at this point in the history
  • Loading branch information
amol- committed May 23, 2013
1 parent e924b4f commit 2da7eaf
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 19 deletions.
105 changes: 105 additions & 0 deletions tests/test_rest_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ def test_put_validation(self):
movie = DBSession.query(Movie).first()
assert movie.title == 'Movie Test'

def test_put_relationship(self):
result = self.app.post('/movies.json', params={'title':'Movie Test'})
movie = result.json['value']

actors = [Actor(name='James Who'), Actor(name='John Doe'), Actor(name='Man Alone')]
map(DBSession.add, actors)
DBSession.flush()
actor_ids = [actor.actor_id for actor in actors[:2]]
transaction.commit()

result = self.app.put('/movies/%s.json' % movie['movie_id'],
params={'title':'Movie Test',
'actors':actor_ids})
assert len(result.json['value']['actors']) == 2, result

assert DBSession.query(Actor).filter_by(movie_id=movie['movie_id']).count() == 2

def test_delete(self):
DBSession.add(Movie(title='Fifth Movie'))
DBSession.flush()
Expand Down Expand Up @@ -173,3 +190,91 @@ def test_get_one___json__(self):
assert result['model'] == 'Actor', result
assert result['value']['name'] == actor.name
assert result['value']['movie_title'] == movie_title


class TestRestJsonReadDictified(CrudTest):
def controller_factory(self):
class MovieController(EasyCrudRestController):
model = Movie
pagination = {'items_per_page': 3}
json_dictify = True

class ActorController(EasyCrudRestController):
model = Actor
json_dictify = True

class RestJsonController(TGController):
movies = MovieController(DBSession)
actors = ActorController(DBSession)

return RestJsonController()

def setUp(self):
super(TestRestJsonReadDictified, self).setUp()
genre = Genre(name='action')
DBSession.add(genre)

actors = [Actor(name='James Who'), Actor(name='John Doe'), Actor(name='Man Alone')]
map(DBSession.add, actors)

DBSession.add(Movie(title='First Movie', genre=genre, actors=actors[:2]))
DBSession.add(Movie(title='Second Movie', genre=genre, actors=actors[:2]))
DBSession.add(Movie(title='Third Movie', genre=genre))
DBSession.add(Movie(title='Fourth Movie', genre=genre))
DBSession.add(Movie(title='Fifth Movie'))
DBSession.add(Movie(title='Sixth Movie'))
DBSession.flush()
transaction.commit()

def test_get_all(self):
result = self.app.get('/movies.json?order_by=movie_id')
result = result.json['value_list']
assert result['total'] == 6, result
assert result['page'] == 1, result
assert result['entries'][0]['title'] == 'First Movie', result
assert len(result['entries'][0]['actors']) == 2, result

result = self.app.get('/movies.json?page=2&order_by=movie_id')
result = result.json['value_list']
assert result['total'] == 6, result
assert result['page'] == 2, result
assert result['entries'][0]['title'] == 'Fourth Movie', result

def test_get_all_filter(self):
actor = DBSession.query(Actor).first()

result = self.app.get('/actors.json?movie_id=%s' % actor.movie_id)
result = result.json['value_list']
assert result['total'] == 2, result

def test_get_all___json__(self):
actor = DBSession.query(Actor).filter(Actor.movie_id!=None).first()
movie_title = actor.movie.title

result = self.app.get('/actors.json?movie_id=%s' % actor.movie_id)
result = result.json['value_list']
assert result['total'] > 0, result

for entry in result['entries']:
assert entry['movie_title'] == movie_title

def test_get_one(self):
movie = DBSession.query(Movie).first()
movie_actors_count = len(movie.actors)

result = self.app.get('/movies/%s.json' % movie.movie_id)
result = result.json
assert result['model'] == 'Movie', result
assert result['value']['title'] == movie.title
assert result['value']['movie_id'] == movie.movie_id
assert len(result['value']['actors']) == movie_actors_count

def test_get_one___json__(self):
actor = DBSession.query(Actor).filter(Actor.movie_id!=None).first()
movie_title = actor.movie.title

result = self.app.get('/actors/%s.json' % actor.actor_id)
result = result.json
assert result['model'] == 'Actor', result
assert result['value']['name'] == actor.name
assert result['value']['movie_title'] == movie_title
57 changes: 39 additions & 18 deletions tgext/crud/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
from tgext.crud.decorators import (registered_validate, register_validators, catch_errors,
optional_paginate)
from tgext.crud.utils import (SmartPaginationCollection, RequestLocalTableFiller, create_setter,
set_table_filler_getter, SortableTableBase, map_args_to_pks)
set_table_filler_getter, SortableTableBase, map_args_to_pks,
adapt_params_for_pagination)
from sprox.providerselector import ProviderTypeSelector
from sprox.fillerbase import TableFiller
from sprox.formbase import AddRecordForm, EditableForm
Expand Down Expand Up @@ -79,6 +80,7 @@ class CrudRestController(RestController):
remember_values = []
substring_filters = []
search_fields = True # True for automagic
json_dictify = False # True is slower but provides relations
pagination = {'items_per_page': 7}
style = Markup('''
#menu_items {
Expand Down Expand Up @@ -200,6 +202,23 @@ def _get_current_search(self, search_fields):
return (field, value)
return (search_fields[0][0], '')

def _dictify(self, value, length=None):
json_dictify = self.json_dictify
if json_dictify is False:
return value

def _dictify(entity):
if hasattr(entity, '__json__'):
return entity.__json__()
else:
return self.provider.dictify(entity, **json_dictify)

if length is not None:
#return a generator, we don't want to consume the whole query if it is paginated
return (_dictify(entity) for entity in value)
else:
return _dictify(value)

def __init__(self, session, menu_items=None):
if menu_items is None:
menu_items = {}
Expand All @@ -209,6 +228,10 @@ def __init__(self, session, menu_items=None):

self.session = session

self.pagination_enabled = (self.pagination and isinstance(self.table_filler, RequestLocalTableFiller))
if self.json_dictify is True:
self.json_dictify = {}

#this makes crc declarative
check_types = ['new_form', 'edit_form', 'table', 'table_filler', 'edit_filler']
for type_ in check_types:
Expand Down Expand Up @@ -241,29 +264,24 @@ def get_all(self, *args, **kw):
paginator.paginate_page = 0

if tg.request.response_type == 'application/json':
count, entries = self.table_filler._do_get_provider_count_and_objs(**kw)
return dict(value_list=entries)
adapt_params_for_pagination(kw, self.pagination_enabled)
count, values = self.table_filler._do_get_provider_count_and_objs(**kw)
values = self._dictify(values, length=count)
if self.pagination_enabled:
values = SmartPaginationCollection(values, count)
return dict(value_list=values)

if not getattr(self.table.__class__, '__retrieves_own_value__', False):
kw.pop('limit', None)
kw.pop('offset', None)
kw.pop('substring_filters', None)

if self.substring_filters is True:
substring_filters = kw.keys()
else:
substring_filters = self.substring_filters

if self.pagination and isinstance(self.table_filler, RequestLocalTableFiller):
paginator = request.paginators['value_list']
page = paginator.paginate_page - 1
values = self.table_filler.get_value(offset=page*paginator.paginate_items_per_page,
limit=paginator.paginate_items_per_page,
substring_filters=substring_filters,
**kw)
adapt_params_for_pagination(kw, self.pagination_enabled)
values = self.table_filler.get_value(substring_filters=substring_filters, **kw)
if self.pagination_enabled:
values = SmartPaginationCollection(values, self.table_filler.__count__)
else:
values = self.table_filler.get_value(substring_filters=substring_filters, **kw)
else:
values = []

Expand All @@ -285,8 +303,9 @@ def get_one(self, *args, **kw):
kw = map_args_to_pks(args, {})

if tg.request.response_type == 'application/json':
obj = self.provider.get_obj(self.model, kw)
return dict(model=self.model.__name__,
value=self.provider.get_obj(self.model, kw))
value=self._dictify(obj))

tmpl_context.widget = self.edit_form
value = self.edit_filler.get_value(kw)
Expand Down Expand Up @@ -322,7 +341,8 @@ def post(self, *args, **kw):
obj = self.provider.create(self.model, params=kw)

if tg.request.response_type == 'application/json':
return dict(model=self.model.__name__, value=obj)
return dict(model=self.model.__name__,
value=self._dictify(obj))

return redirect('./', params=self._kept_params())

Expand All @@ -344,7 +364,8 @@ def put(self, *args, **kw):

obj = self.provider.update(self.model, params=kw, omit_fields=omit_fields)
if tg.request.response_type == 'application/json':
return dict(model=self.model.__name__, value=obj)
return dict(model=self.model.__name__,
value=self._dictify(obj))

pks = self.provider.get_primary_fields(self.model)
return redirect('../' * len(pks), params=self._kept_params())
Expand Down
13 changes: 12 additions & 1 deletion tgext/crud/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,15 @@ def map_args_to_pks(remainder, params):
if pk not in params and i < len(remainder):
params[pk] = remainder[i]

return params
return params

def adapt_params_for_pagination(params, pagination_enabled=True):
params.pop('limit', None)
params.pop('offset', None)

if pagination_enabled:
paginator = request.paginators['value_list']
page = paginator.paginate_page - 1

params['offset'] = page*paginator.paginate_items_per_page
params['limit'] = paginator.paginate_items_per_page

2 comments on commit 2da7eaf

@moschlar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh...
I must say, having to explicitly change the return values of the controller methods for dictification seems a bit ugly...
Wouldn't this be nicer with a decorator which dictifies the values if needed?

@amol-
Copy link
Member Author

@amol- amol- commented on 2da7eaf May 26, 2013

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dunno, in this case I like that fact that dictification is explicit, it makes clear what gets dictified and doesn't touch values returned by users in case of subclassing. I usually tend to avoid decorators that alter the response when the response can be generated by library users.

Please sign in to comment.