Skip to content
This repository has been archived by the owner on Oct 11, 2022. It is now read-only.

Commit

Permalink
Added pagination for contacts and fake contacts + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Rudi Giesler committed Sep 2, 2014
1 parent e6009ac commit 3f685e5
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 8 deletions.
55 changes: 48 additions & 7 deletions go_contacts/backends/contacts.py
Expand Up @@ -13,6 +13,8 @@
from go_api.collections.errors import (
CollectionObjectNotFound, CollectionUsageError)

import itertools


def contact_to_dict(contact):
"""
Expand Down Expand Up @@ -47,6 +49,7 @@ def get_contact_collection(self, owner_id):
class RiakContactsCollection(object):
def __init__(self, contact_store, max_contacts_per_page):
self.contact_store = contact_store
self.max_contacts_per_page = max_contacts_per_page

@staticmethod
def _pick_fields(data, keys):
Expand Down Expand Up @@ -86,6 +89,16 @@ def all_keys(self):
"""
raise NotImplementedError()

@inlineCallbacks
def _get_all_contacts(self):
contact_keys = yield self.contact_store.list_contacts()
contact_keys = contact_keys or []
contact_list = []
for key in contact_keys:
contact = yield self.contact_store.get_contact_by_key(key)
contact_list.append(contact)
returnValue(contact_list)

@inlineCallbacks
def stream(self, query):
"""
Expand All @@ -99,14 +112,26 @@ def stream(self, query):
"""
if query is not None:
raise CollectionUsageError("query parameter not supported")
contact_keys = yield self.contact_store.list_contacts()
contact_keys = contact_keys or []
contact_list = []
for key in contact_keys:
contact = yield self.contact_store.get_contact_by_key(key)
contact_list.append(contact)
contact_list = yield self._get_all_contacts()
returnValue([map(contact_to_dict, contact_list)])

def _paginate(self, contact_list, cursor, max_results):
contact_list.sort(key=lambda contact: contact.key)
if cursor is not None:
contact_list = list(itertools.dropwhile(
lambda contact: contact.key <= cursor, contact_list))
new_cursor = None
if len(contact_list) > max_results:
contact_list = contact_list[:max_results]
new_cursor = contact_list[-1].key
return (contact_list, new_cursor)

def _encode_cursor(self, cursor):
if cursor is not None:
cursor = cursor.encode('rot13')
return cursor

@inlineCallbacks
def page(self, cursor, max_results, query):
"""
Generages a page which contains a subset of the objects in the
Expand All @@ -128,7 +153,23 @@ def page(self, cursor, max_results, query):
list of all the objects within the page.
:rtype: tuple
"""
raise NotImplementedError()
# TODO: Use riak pagination instead of fake pagination
if query is not None:
raise CollectionUsageError("query parameter not supported")

max_results = max_results or float('inf')
max_results = min(max_results, self.max_contacts_per_page)

# Encoding and decoding are the same operation
cursor = self._encode_cursor(cursor)
contact_list = yield self._get_all_contacts()

(contact_list, cursor) = self._paginate(
contact_list, cursor, max_results)

cursor = self._encode_cursor(cursor)
contact_list = map(contact_to_dict, contact_list)
returnValue((cursor, contact_list))

@inlineCallbacks
def get(self, object_id):
Expand Down
160 changes: 160 additions & 0 deletions go_contacts/tests/server_contacts_test_mixin.py
Expand Up @@ -275,13 +275,21 @@ def test_stream_contacts_query(self):

@inlineCallbacks
def test_stream_all_contacts_empty(self):
"""
This test ensures that an empty list of data is streamed if there are
no contacts in the contact store.
"""
api = self.mk_api()
(code, data) = yield self.request(api, 'GET', '/contacts/?stream=true')
self.assertEqual(code, 200)
self.assertEqual(data, [])

@inlineCallbacks
def test_stream_all_contacts(self):
"""
This test ensures that all the contacts in the contact store are
streamed when streaming is requested.
"""
api = self.mk_api()
contact1 = yield self.create_contact(
api, name=u"Bob", msisdn=u"+12345")
Expand All @@ -292,3 +300,155 @@ def test_stream_all_contacts(self):
self.assertEqual(code, 200)
self.assertTrue(contact1 in data)
self.assertTrue(contact2 in data)

@inlineCallbacks
def test_get_contact_empty_page(self):
"""
This tests tests that an empty page is returned when there are no
contacts in the contact store.
"""
api = self.mk_api()
(code, data) = yield self.request(api, 'GET', '/contacts/')
self.assertEqual(code, 200)
self.assertEqual(data, {'cursor': None, 'data': []})

@inlineCallbacks
def test_get_contact_page_multiple(self):
"""
This test ensures that contacts are split over multiple pages according
to the ``max_results`` parameter in the query string. It also tests
that multiple pages are fetched correctly when using the next page
cursor
"""
api = self.mk_api()

contact1 = yield self.create_contact(
api, name=u"Bob", msisdn=u"+12345")
contact2 = yield self.create_contact(
api, name=u"Susan", msisdn=u"+54321")
contact3 = yield self.create_contact(
api, name=u"Foo", msisdn=u"+314159")

(code, data) = yield self.request(
api, 'GET', '/contacts/?max_results=2')
self.assertEqual(code, 200)
cursor = data[u'cursor']
contacts = data[u'data']
self.assertEqual(len(contacts), 2)

(code, data) = yield self.request(
api, 'GET',
'/contacts/?max_results=2&cursor=%s' % cursor.encode('ascii'))
self.assertEqual(code, 200)
self.assertEqual(data[u'cursor'], None)
contacts += data[u'data']

self.assertTrue(contact1 in contacts)
self.assertTrue(contact2 in contacts)
self.assertTrue(contact3 in contacts)

@inlineCallbacks
def test_get_contact_page_single(self):
"""
This test tests that a single page with a None cursor is returned if
all the contacts in the contact store fit into one page.
"""
api = self.mk_api()

contact1 = yield self.create_contact(
api, name=u"Bob", msisdn=u"+12345")
contact2 = yield self.create_contact(
api, name=u"Susan", msisdn=u"+54321")

(code, data) = yield self.request(
api, 'GET', '/contacts/?max_results=2')
self.assertEqual(code, 200)
cursor = data[u'cursor']
contacts = data[u'data']
self.assertEqual(len(contacts), 2)
self.assertEqual(cursor, None)
self.assertTrue(contact1 in contacts)
self.assertTrue(contact2 in contacts)

@inlineCallbacks
def test_page_default_limit_contacts(self):
"""
For this test, the default limit per page is set to 5. If the user
requests more than this, `contacts/?max_results=10`, it should only
return the maximum of 5 results per page.
"""
api = self.mk_api_lim_5()

for i in range(10):
yield self.create_contact(
api, name=u'%s' % str(i)*5, msisdn=u'+%s' % str(i)*5)

(code, data) = yield self.request(
api, 'GET', '/contacts/?max_results=10')
self.assertEqual(code, 200)
self.assertEqual(len(data.get('data')), 5)

@inlineCallbacks
def test_page_bad_cursor(self):
"""
If the user requests a next page cursor that doesn't exist,
``contacts/?cursor=bad-id``, an empty page should be returned
"""
api = self.mk_api()
yield self.create_contact(api, name=u"Bob", msisdn=u"+12345")

(code, data) = yield self.request(
api, 'GET', '/contacts/?cursor=bad-id')
self.assertEqual(code, 200)
self.assertEqual(data.get(u'cursor'), None)
self.assertEqual(data.get(u'data'), [])

@inlineCallbacks
def test_page_deleted_cursor(self):
"""
If the contact linked to the cursor is deleted, it should still return
the next page.
"""
api = self.mk_api()
keys = []
for i in range(4):
contact = yield self.create_contact(
api, name=u'%s' % str(i)*5, msisdn=u'+%s' % str(i)*5)
keys.append(contact.get('key'))
keys.sort()
(code, deleted_contact) = yield self.request(
api, 'DELETE', '/contacts/%s' % keys[1])
(code, data) = yield self.request(
api, 'GET', '/contacts/?max_items=2&cursor=%s' %
deleted_contact['key'].encode('rot13'))
self.assertEqual(code, 200)
self.assertEqual(data['data'][0]['key'], keys[2])

@inlineCallbacks
def test_page_query(self):
"""
If a query parameter is supplied, a CollectionUsageError should be
thrown, as querys are not yet supported.
"""
api = self.mk_api()
(code, data) = yield self.request(api, 'GET', '/contacts/?query=foo')
self.assertEqual(code, 400)
self.assertEqual(data.get(u'status_code'), 400)
self.assertEqual(data.get(u'reason'), u'query parameter not supported')

@inlineCallbacks
def test_page_cursor_encoding(self):
"""
The cursor should be the ROT13 encoding of the key of the last contact
on the page.
"""
api = self.mk_api()
keys = []
for i in range(3):
contact = yield self.create_contact(
api, name=u'%s' % str(i)*5, msisdn=u'+%s' % str(i)*5)
keys.append(contact.get('key'))
keys.sort()
(code, data) = yield self.request(
api, 'GET', '/contacts/?max_results=2')
self.assertEqual(data.get('cursor'), keys[1].encode('rot13'))
13 changes: 13 additions & 0 deletions go_contacts/tests/test_server.py
Expand Up @@ -109,6 +109,16 @@ def mk_api(self):
})
return ContactsApi(configfile)

def mk_api_lim_5(self):
configfile = self.mk_config({
"riak_manager": {
"bucket_prefix": "test",
},
"max_contacts_per_page": 5,
"max_groups_per_page": 5,
})
return ContactsApi(configfile)

@inlineCallbacks
def request(self, api, method, path, body=None, headers=None, auth=True):
if headers is None:
Expand Down Expand Up @@ -176,6 +186,9 @@ def setUp(self):
def mk_api(self):
return self.api_class("", "token-1", {})

def mk_api_lim_5(self):
return self.api_class("", "token-1", {}, {}, 5, 5)

def request(self, api, method, path, body=None, headers=None, auth=True):
if headers is None:
headers = {}
Expand Down
37 changes: 36 additions & 1 deletion verified-fake/fake_go_contacts.py
Expand Up @@ -123,9 +123,44 @@ def get_all(self, query):
stream = query.get('stream', None)
stream = stream and stream[0]
q = query.get('query', None)
q = q and [0]
q = q and q[0]
if stream == 'true':
return self.get_all_contacts(q)
else:
cursor = query.get('cursor', None)
cursor = cursor and cursor[0]
max_results = query.get('max_results', None)
max_results = max_results and max_results[0]
return self.get_page_contacts(q, cursor, max_results)

def _paginate(self, contact_list, cursor, max_results):
contact_list.sort(key=lambda contact: contact['key'])
if cursor is not None:
contact_list = list(itertools.dropwhile(
lambda contact: contact['key'] <= cursor, contact_list))
new_cursor = None
if len(contact_list) > max_results:
contact_list = contact_list[:max_results]
new_cursor = contact_list[-1]['key']
return (contact_list, new_cursor)

def _encode_cursor(self, cursor):
if cursor is not None:
cursor = cursor.encode('rot13')
return cursor

def get_page_contacts(self, query, cursor, max_results):
contacts = self.get_all_contacts(query)

# Encoding and decoding are the same operation
cursor = self._encode_cursor(cursor)
max_results = (max_results and int(max_results)) or float('inf')
max_results = min(max_results, self.max_contacts_per_page)

(contacts, cursor) = self._paginate(contacts, cursor, max_results)

cursor = self._encode_cursor(cursor)
return {u'cursor': cursor, u'data': contacts}

def update_contact(self, contact_key, contact_data):
contact = self.get_contact(contact_key)
Expand Down

0 comments on commit 3f685e5

Please sign in to comment.