Merge lp:~fgallina/rnr-server/paginated-handler into lp:rnr-server

Proposed by Fabián Ezequiel Gallina
Status: Merged
Approved by: Fabián Ezequiel Gallina
Approved revision: 277
Merged at revision: 274
Proposed branch: lp:~fgallina/rnr-server/paginated-handler
Merge into: lp:rnr-server
Diff against target: 235 lines (+222/-0)
2 files modified
src/core/api/pagination.py (+77/-0)
src/core/tests/test_pagination.py (+145/-0)
To merge this branch: bzr merge lp:~fgallina/rnr-server/paginated-handler
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Review via email: mp+232712@code.launchpad.net

Commit message

Added pagination support for piston handlers

Description of the change

Added pagination support for piston handlers

To post a comment you must log in.
275. By Fabián Ezequiel Gallina

Promote PaginatedHandlerMixin.make_paginated_response to a function

276. By Fabián Ezequiel Gallina

Rename TestCase

Revision history for this message
James Westby (james-w) wrote :

Hi,

Thanks for making that change. I would move the functions to src/core/api/pagination.py and make them all public, but that's up to you whether you agree.

Thanks,

James

review: Approve
277. By Fabián Ezequiel Gallina

Move API pagination functions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'src/core/api'
2=== added file 'src/core/api/__init__.py'
3=== added file 'src/core/api/pagination.py'
4--- src/core/api/pagination.py 1970-01-01 00:00:00 +0000
5+++ src/core/api/pagination.py 2014-08-29 19:35:29 +0000
6@@ -0,0 +1,77 @@
7+import urllib
8+import urlparse
9+
10+from django.core.paginator import (
11+ EmptyPage,
12+ InvalidPage,
13+ PageNotAnInteger,
14+ Paginator
15+)
16+
17+
18+def _get_page_url(request, page_number_fn):
19+ absolute_uri = request.build_absolute_uri()
20+ uri_split = urlparse.urlsplit(absolute_uri)
21+ query_dict = urlparse.parse_qs(uri_split.query)
22+
23+ try:
24+ page_number = page_number_fn()
25+ query_dict['page'] = [page_number]
26+ query = urllib.urlencode(query_dict, doseq=True)
27+ page_url = urlparse.SplitResult(uri_split.scheme,
28+ uri_split.netloc,
29+ uri_split.path,
30+ query,
31+ uri_split.fragment).geturl()
32+ except InvalidPage:
33+ page_url = None
34+
35+ return page_url
36+
37+
38+def _get_previous_page_url(request, page):
39+ return _get_page_url(request, page.previous_page_number)
40+
41+
42+def _get_next_page_url(request, page):
43+ return _get_page_url(request, page.next_page_number)
44+
45+
46+def make_paginated_response(
47+ request, queryset, page_size=20, max_page_size=100):
48+ """Return paginated results with pagination data.
49+
50+ This is the main entry point for the mixin and must be
51+ explicitly called with the desired queryset/object list to be
52+ paginated.
53+
54+ Args:
55+ request: A request object.
56+ queryset: A QuerySet or iterable to be paginated.
57+ page_size: The number of results per page.
58+ max_page_size: The max number of results per page.
59+ Results are unlimited when None.
60+
61+ Returns:
62+ A Dict including links to previous/next page, the object
63+ list count for current page, the total number of results
64+ and the results for the current page.
65+ """
66+ page_size = request.GET.get('page_size', page_size)
67+ if max_page_size is not None:
68+ page_size = min(page_size, max_page_size)
69+
70+ paginator = Paginator(queryset, page_size)
71+ page = request.GET.get('page', 1)
72+ try:
73+ page = paginator.page(page)
74+ except PageNotAnInteger:
75+ page = paginator.page(1)
76+ except EmptyPage:
77+ page = paginator.page(paginator.num_pages)
78+
79+ # 'django-rest-framework'-like serialization
80+ return {'previous': _get_previous_page_url(request, page),
81+ 'next': _get_next_page_url(request, page),
82+ 'count': len(page.object_list),
83+ 'results': page.object_list}
84
85=== added directory 'src/core/tests'
86=== added file 'src/core/tests/__init__.py'
87=== added file 'src/core/tests/test_pagination.py'
88--- src/core/tests/test_pagination.py 1970-01-01 00:00:00 +0000
89+++ src/core/tests/test_pagination.py 2014-08-29 19:35:29 +0000
90@@ -0,0 +1,145 @@
91+import urlparse
92+
93+from django.test import TestCase
94+from mock import Mock
95+
96+from core.api.pagination import make_paginated_response
97+
98+
99+class MakePaginatedResponseTestCase(TestCase):
100+
101+ maxDiff = None
102+
103+ def setUp(self):
104+ super(MakePaginatedResponseTestCase, self).setUp()
105+ self.request = self.make_request()
106+
107+ def make_request(self, page_size=None, page=None, uri=None):
108+ request = Mock()
109+ request.GET = {}
110+
111+ if page_size is not None:
112+ request.GET['page_size'] = page_size
113+
114+ if page is not None:
115+ request.GET['page'] = page
116+
117+ if uri is None:
118+ uri = 'http://someserver.org/api/objects/'
119+
120+ request.build_absolute_uri = lambda location=None: uri
121+ return request
122+
123+ def test_first_page(self):
124+ objects = range(100)
125+ request = self.make_request(page=1)
126+ response = make_paginated_response(request, objects)
127+ self.assertEqual(response, {
128+ 'previous': None,
129+ 'next': self.request.build_absolute_uri() + '?page=2',
130+ 'count': 20,
131+ 'results': range(20)})
132+
133+ def test_no_page_gives_first_page(self):
134+ objects = range(100)
135+ response = make_paginated_response(self.request, objects)
136+ self.assertEqual(response, {
137+ 'previous': None,
138+ 'next': self.request.build_absolute_uri() + '?page=2',
139+ 'count': 20,
140+ 'results': range(20)})
141+
142+ def test_next_page(self):
143+ objects = range(100)
144+ request = self.make_request(page=2)
145+ response = make_paginated_response(request, objects)
146+ self.assertEqual(response, {
147+ 'previous': self.request.build_absolute_uri() + '?page=1',
148+ 'next': self.request.build_absolute_uri() + '?page=3',
149+ 'count': 20,
150+ 'results': range(20, 40)})
151+
152+ def test_last_page(self):
153+ objects = range(21)
154+ request = self.make_request(page=2)
155+ response = make_paginated_response(request, objects)
156+ self.assertEqual(response, {
157+ 'previous': self.request.build_absolute_uri() + '?page=1',
158+ 'next': None,
159+ 'count': 1,
160+ 'results': [20]})
161+
162+ def test_unlimited_results(self):
163+ objects = range(1000)
164+ request = self.make_request(page_size=2000)
165+ response = make_paginated_response(
166+ request, objects, max_page_size=None)
167+ self.assertEqual(response, {
168+ 'previous': None,
169+ 'next': None,
170+ 'count': 1000,
171+ 'results': objects})
172+
173+ def test_max_page_size(self):
174+ objects = range(1000)
175+ request = self.make_request(page_size=2000)
176+ response = make_paginated_response(
177+ request, objects, max_page_size=20)
178+ self.assertEqual(response, {
179+ 'previous': None,
180+ 'next': request.build_absolute_uri() + '?page=2',
181+ 'count': 20,
182+ 'results': range(20)})
183+
184+ def test_default_page_size(self):
185+ objects = range(100)
186+ response = make_paginated_response(
187+ self.request, objects, page_size=50)
188+ self.assertEqual(response, {
189+ 'previous': None,
190+ 'next': self.request.build_absolute_uri() + '?page=2',
191+ 'count': 50,
192+ 'results': range(50)})
193+
194+ def test_invalid_page_uses_first(self):
195+ objects = range(100)
196+ request = self.make_request(page='invalid')
197+ response = make_paginated_response(request, objects)
198+ self.assertEqual(response, {
199+ 'previous': None,
200+ 'next': request.build_absolute_uri() + '?page=2',
201+ 'count': 20,
202+ 'results': range(20)})
203+
204+ def test_empty_page_uses_last_page(self):
205+ objects = range(100)
206+ # There are only 5 pages of 20 objects each, and user requests
207+ # the 200th page, she should get the last page instead.
208+ request = self.make_request(page=200)
209+ response = make_paginated_response(request, objects)
210+ self.assertEqual(response, {
211+ 'previous': request.build_absolute_uri() + '?page=4',
212+ 'next': None,
213+ 'count': 20,
214+ 'results': range(80, 100)})
215+
216+ def test_next_and_previous_links_respect_query_params(self):
217+ objects = range(100)
218+ uri = 'http://someserver.org/api/objects/?a=1&b=2&b=3&c=4'
219+ request = self.make_request(page=2, uri=uri)
220+ response = make_paginated_response(request, objects)
221+
222+ self.assertEqual(response['count'], 20)
223+ self.assertEqual(response['results'], range(20, 40))
224+
225+ # existing query params must be in next and previous links
226+ expected_previous_qs = urlparse.parse_qs(
227+ urlparse.urlsplit(uri + '&page=1').query)
228+ expected_next_qs = urlparse.parse_qs(
229+ urlparse.urlsplit(uri + '&page=3').query)
230+ previous_qs = urlparse.parse_qs(
231+ urlparse.urlsplit(response['previous']).query)
232+ next_qs = urlparse.parse_qs(
233+ urlparse.urlsplit(response['next']).query)
234+ self.assertEqual(previous_qs, expected_previous_qs)
235+ self.assertEqual(next_qs, expected_next_qs)

Subscribers

People subscribed via source and target branches