Barry Warsaw pushed to branch master at mailman / Mailman

Commits:
a5b65681 by Aurélien Bompard at 2015-11-04T11:33:07Z
Rework pagination to fix the 'start' and 'total_size' values

- - - - -


10 changed files:

- src/mailman/rest/docs/lists.rst
- src/mailman/rest/docs/membership.rst
- src/mailman/rest/docs/users.rst
- src/mailman/rest/helpers.py
- src/mailman/rest/lists.py
- src/mailman/rest/members.py
- src/mailman/rest/queues.py
- src/mailman/rest/tests/test_lists.py
- src/mailman/rest/tests/test_paginate.py
- src/mailman/rest/users.py


Changes:

=====================================
src/mailman/rest/docs/lists.rst
=====================================
--- a/src/mailman/rest/docs/lists.rst
+++ b/src/mailman/rest/docs/lists.rst
@@ -76,7 +76,7 @@ page.
         volume: 1
     http_etag: "..."
     start: 0
-    total_size: 1
+    total_size: 2
 
     >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
     ...           '?count=1&page=2')
@@ -91,8 +91,8 @@ page.
         self_link: http://localhost:9001/3.0/lists/bird.example.com
         volume: 1
     http_etag: "..."
-    start: 0
-    total_size: 1
+    start: 1
+    total_size: 2
 
 
 Creating lists via the API


=====================================
src/mailman/rest/docs/membership.rst
=====================================
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -267,7 +267,7 @@ page.
         user: http://localhost:9001/3.0/users/3
     http_etag: ...
     start: 0
-    total_size: 1
+    total_size: 2
 
 This works with members of a single list as well as with all members.
 
@@ -285,7 +285,7 @@ This works with members of a single list as well as with 
all members.
         user: http://localhost:9001/3.0/users/3
     http_etag: ...
     start: 0
-    total_size: 1
+    total_size: 5
 
 
 Owners and moderators


=====================================
src/mailman/rest/docs/users.rst
=====================================
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -84,7 +84,7 @@ page.
         user_id: 1
     http_etag: "..."
     start: 0
-    total_size: 1
+    total_size: 2
 
     >>> dump_json('http://localhost:9001/3.0/users?count=1&page=2')
     entry 0:
@@ -94,8 +94,8 @@ page.
         self_link: http://localhost:9001/3.0/users/2
         user_id: 2
     http_etag: "..."
-    start: 0
-    total_size: 1
+    start: 1
+    total_size: 2
 
 
 Creating users


=====================================
src/mailman/rest/helpers.py
=====================================
--- a/src/mailman/rest/helpers.py
+++ b/src/mailman/rest/helpers.py
@@ -114,31 +114,6 @@ def etag(resource):
     return json.dumps(resource, cls=ExtendedEncoder)
 
 
-def paginate(method):
-    """Method decorator to paginate through collection result lists.
-
-    Use this to return only a slice of a collection, specified in the request
-    itself.  The request should use query parameters `count` and `page` to
-    specify the slice they want.  The slice will start at index
-    ``(page - 1) * count`` and end (exclusive) at ``(page * count)``.
-
-    Decorated methods must take ``self`` and ``request`` as the first two
-    arguments.
-    """
-    def wrapper(self, request, *args, **kwargs):
-        # Allow falcon's HTTPBadRequest exceptions to percolate up.  They'll
-        # get turned into HTTP 400 errors.
-        count = request.get_param_as_int('count', min=0)
-        page = request.get_param_as_int('page', min=1)
-        result = method(self, request, *args, **kwargs)
-        if count is None and page is None:
-            return result
-        list_start = (page - 1) * count
-        list_end = page * count
-        return result[list_start:list_end]
-    return wrapper
-
-
 
 class CollectionMixin:
     """Mixin class for common collection-ish things."""
@@ -170,22 +145,38 @@ class CollectionMixin:
         """
         raise NotImplementedError
 
+    def _paginate(self, request, collection):
+        """Method to paginate through collection result lists.
+
+        Use this to return only a slice of a collection, specified in the 
request
+        itself.  The request should use query parameters `count` and `page` to
+        specify the slice they want.  The slice will start at index
+        ``(page - 1) * count`` and end (exclusive) at ``(page * count)``.
+        """
+        # Allow falcon's HTTPBadRequest exceptions to percolate up.  They'll
+        # get turned into HTTP 400 errors.
+        count = request.get_param_as_int('count', min=0)
+        page = request.get_param_as_int('page', min=1)
+        total_size = len(collection)
+        if count is None and page is None:
+            return 0, total_size, collection
+        list_start = (page - 1) * count
+        list_end = page * count
+        return list_start, total_size, collection[list_start:list_end]
+
     def _make_collection(self, request):
         """Provide the collection to the REST layer."""
-        collection = self._get_collection(request)
-        if len(collection) == 0:
-            return dict(start=0, total_size=0)
-        else:
+        start, total_size, collection = self._paginate(
+            request, self._get_collection(request))
+        result = dict(start=start, total_size=total_size)
+        if len(collection) != 0:
             entries = [self._resource_as_dict(resource)
                        for resource in collection]
             # Tag the resources but use the dictionaries.
             [etag(resource) for resource in entries]
             # Create the collection resource
-            return dict(
-                start=0,
-                total_size=len(collection),
-                entries=entries,
-                )
+            result['entries'] = entries
+        return result
 
     def path_to(self, resource):
         return path_to(resource, self.api_version)


=====================================
src/mailman/rest/lists.py
=====================================
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -40,7 +40,7 @@ from mailman.interfaces.subscriptions import 
ISubscriptionService
 from mailman.rest.listconf import ListConfiguration
 from mailman.rest.helpers import (
     CollectionMixin, GetterSetter, NotFound, bad_request, child, created,
-    etag, no_content, not_found, okay, paginate)
+    etag, no_content, not_found, okay)
 from mailman.rest.members import AMember, MemberCollection
 from mailman.rest.post_moderation import HeldMessages
 from mailman.rest.sub_moderation import SubscriptionRequests
@@ -112,7 +112,6 @@ class _ListBase(CollectionMixin):
             self_link=self.path_to('lists/{0}'.format(mlist.list_id)),
             )
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return list(getUtility(IListManager))
@@ -231,7 +230,6 @@ class MembersOfList(MemberCollection):
         self._mlist = mailing_list
         self._role = role
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         # Overrides _MemberBase._get_collection() because we only want to
@@ -252,7 +250,6 @@ class ListsForDomain(_ListBase):
         resource = self._make_collection(request)
         okay(response, etag(resource))
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return list(self._domain.mailing_lists)


=====================================
src/mailman/rest/members.py
=====================================
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -38,7 +38,7 @@ from mailman.interfaces.user import IUser, 
UnverifiedAddressError
 from mailman.interfaces.usermanager import IUserManager
 from mailman.rest.helpers import (
     CollectionMixin, NotFound, accepted, bad_request, child, conflict,
-    created, etag, no_content, not_found, okay, paginate)
+    created, etag, no_content, not_found, okay)
 from mailman.rest.preferences import Preferences, ReadOnlyPreferences
 from mailman.rest.validator import (
     Validator, enum_validator, subscriber_validator)
@@ -77,7 +77,6 @@ class _MemberBase(CollectionMixin):
                 'users/{}'.format(user.user_id.int))
         return response
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return list(getUtility(ISubscriptionService))


=====================================
src/mailman/rest/queues.py
=====================================
--- a/src/mailman/rest/queues.py
+++ b/src/mailman/rest/queues.py
@@ -28,8 +28,7 @@ from mailman.config import config
 from mailman.app.inject import inject_text
 from mailman.interfaces.listmanager import IListManager
 from mailman.rest.helpers import (
-    CollectionMixin, bad_request, created, etag, no_content, not_found, okay,
-    paginate)
+    CollectionMixin, bad_request, created, etag, no_content, not_found, okay)
 from mailman.rest.validator import Validator
 from zope.component import getUtility
 
@@ -50,7 +49,6 @@ class _QueuesBase(CollectionMixin):
             self_link=self.path_to('queues/{}'.format(name)),
             )
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return sorted(config.switchboards)


=====================================
src/mailman/rest/tests/test_lists.py
=====================================
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -287,7 +287,7 @@ class TestListPagination(unittest.TestCase):
             'http://localhost:9001/3.0/domains/example.com/lists'
             '?count=1&page=1')
         # There are 6 total lists, but only the first one in the page.
-        self.assertEqual(resource['total_size'], 1)
+        self.assertEqual(resource['total_size'], 6)
         self.assertEqual(resource['start'], 0)
         self.assertEqual(len(resource['entries']), 1)
         entry = resource['entries'][0]
@@ -298,8 +298,8 @@ class TestListPagination(unittest.TestCase):
             'http://localhost:9001/3.0/domains/example.com/lists'
             '?count=1&page=2')
         # There are 6 total lists, but only the first one in the page.
-        self.assertEqual(resource['total_size'], 1)
-        self.assertEqual(resource['start'], 0)
+        self.assertEqual(resource['total_size'], 6)
+        self.assertEqual(resource['start'], 1)
         self.assertEqual(len(resource['entries']), 1)
         entry = resource['entries'][0]
         self.assertEqual(entry['fqdn_listname'], 'b...@example.com')
@@ -309,8 +309,8 @@ class TestListPagination(unittest.TestCase):
             'http://localhost:9001/3.0/domains/example.com/lists'
             '?count=1&page=6')
         # There are 6 total lists, but only the first one in the page.
-        self.assertEqual(resource['total_size'], 1)
-        self.assertEqual(resource['start'], 0)
+        self.assertEqual(resource['total_size'], 6)
+        self.assertEqual(resource['start'], 5)
         self.assertEqual(len(resource['entries']), 1)
         entry = resource['entries'][0]
         self.assertEqual(entry['fqdn_listname'], 'f...@example.com')
@@ -337,6 +337,6 @@ class TestListPagination(unittest.TestCase):
             'http://localhost:9001/3.0/domains/example.com/lists'
             '?count=1&page=7')
         # There are 6 total lists, but only the first one in the page.
-        self.assertEqual(resource['total_size'], 0)
-        self.assertEqual(resource['start'], 0)
+        self.assertEqual(resource['total_size'], 6)
+        self.assertEqual(resource['start'], 6)
         self.assertNotIn('entries', resource)


=====================================
src/mailman/rest/tests/test_paginate.py
=====================================
--- a/src/mailman/rest/tests/test_paginate.py
+++ b/src/mailman/rest/tests/test_paginate.py
@@ -27,7 +27,7 @@ import unittest
 from falcon import HTTPInvalidParam, Request
 from mailman.app.lifecycle import create_list
 from mailman.database.transaction import transaction
-from mailman.rest.helpers import paginate
+from mailman.rest.helpers import CollectionMixin
 from mailman.testing.layers import RESTLayer
 
 
@@ -51,79 +51,84 @@ class TestPaginateHelper(unittest.TestCase):
         with transaction():
             self._mlist = create_list('t...@example.com')
 
+    def _get_resource(self):
+        class Resource(CollectionMixin):
+            def _get_collection(self, request):
+                return ['one', 'two', 'three', 'four', 'five']
+            def _resource_as_dict(self, res):
+                return {'value': res}
+        return Resource()
+
     def test_no_pagination(self):
         # When there is no pagination params in the request, all 5 items in
         # the collection are returned.
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
+        resource = self._get_resource()
         # Expect 5 items
-        page = get_collection(None, _FakeRequest())
-        self.assertEqual(page, ['one', 'two', 'three', 'four', 'five'])
+        page = resource._make_collection(_FakeRequest())
+        self.assertEqual(page['start'], 0)
+        self.assertEqual(page['total_size'], 5)
+        self.assertEqual(
+            [entry['value'] for entry in page['entries']],
+            ['one', 'two', 'three', 'four', 'five'])
 
     def test_valid_pagination_request_page_one(self):
         # ?count=2&page=1 returns the first page, with two items in it.
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        page = get_collection(None, _FakeRequest(2, 1))
-        self.assertEqual(page, ['one', 'two'])
+        resource = self._get_resource()
+        page = resource._make_collection(_FakeRequest(2, 1))
+        self.assertEqual(page['start'], 0)
+        self.assertEqual(page['total_size'], 5)
+        self.assertEqual(
+            [entry['value'] for entry in page['entries']], ['one', 'two'])
 
     def test_valid_pagination_request_page_two(self):
         # ?count=2&page=2 returns the second page, where a page has two items
         # in it.
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        page = get_collection(None, _FakeRequest(2, 2))
-        self.assertEqual(page, ['three', 'four'])
+        resource = self._get_resource()
+        page = resource._make_collection(_FakeRequest(2, 2))
+        self.assertEqual(page['start'], 2)
+        self.assertEqual(page['total_size'], 5)
+        self.assertEqual(
+            [entry['value'] for entry in page['entries']], ['three', 'four'])
 
     def test_2nd_index_larger_than_total(self):
         # ?count=2&page=3 returns the third page with page size 2, but the
         # last page only has one item in it.
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        page = get_collection(None, _FakeRequest(2, 3))
-        self.assertEqual(page, ['five'])
+        resource = self._get_resource()
+        page = resource._make_collection(_FakeRequest(2, 3))
+        self.assertEqual(page['start'], 4)
+        self.assertEqual(page['total_size'], 5)
+        self.assertEqual(
+            [entry['value'] for entry in page['entries']], ['five'])
 
     def test_out_of_range_returns_empty_list(self):
         # ?count=2&page=4 returns the fourth page, which doesn't exist, so an
         # empty collection is returned.
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        page = get_collection(None, _FakeRequest(2, 4))
-        self.assertEqual(page, [])
+        resource = self._get_resource()
+        page = resource._make_collection(_FakeRequest(2, 4))
+        self.assertEqual(page['start'], 6)
+        self.assertEqual(page['total_size'], 5)
+        self.assertNotIn('entries', page)
 
     def test_count_as_string_returns_bad_request(self):
         # ?count=two&page=2 are not valid values, so a bad request occurs.
-        @paginate
-        def get_collection(self, request):
-            return []
-        self.assertRaises(HTTPInvalidParam, get_collection,
-                          None, _FakeRequest('two', 1))
+        resource = self._get_resource()
+        self.assertRaises(HTTPInvalidParam, resource._make_collection,
+                          _FakeRequest('two', 1))
 
     def test_negative_count(self):
         # ?count=-1&page=1
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        self.assertRaises(HTTPInvalidParam, get_collection,
-                          None, _FakeRequest(-1, 1))
+        resource = self._get_resource()
+        self.assertRaises(HTTPInvalidParam, resource._make_collection,
+                          _FakeRequest(-1, 1))
 
     def test_negative_page(self):
         # ?count=1&page=-1
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        self.assertRaises(HTTPInvalidParam, get_collection,
-                          None, _FakeRequest(1, -1))
+        resource = self._get_resource()
+        self.assertRaises(HTTPInvalidParam, resource._make_collection,
+                          _FakeRequest(1, -1))
 
     def test_negative_page_and_count(self):
         # ?count=1&page=-1
-        @paginate
-        def get_collection(self, request):
-            return ['one', 'two', 'three', 'four', 'five']
-        self.assertRaises(HTTPInvalidParam, get_collection,
-                          None, _FakeRequest(-1, -1))
+        resource = self._get_resource()
+        self.assertRaises(HTTPInvalidParam, resource._make_collection,
+                          _FakeRequest(-1, -1))


=====================================
src/mailman/rest/users.py
=====================================
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -35,8 +35,7 @@ from mailman.interfaces.usermanager import IUserManager
 from mailman.rest.addresses import UserAddresses
 from mailman.rest.helpers import (
     BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child,
-    conflict, created, etag, forbidden, no_content, not_found, okay, paginate,
-    path_to)
+    conflict, created, etag, forbidden, no_content, not_found, okay, path_to)
 from mailman.rest.preferences import Preferences
 from mailman.rest.validator import (
     PatchValidator, Validator, list_of_strings_validator)
@@ -149,7 +148,6 @@ class _UserBase(CollectionMixin):
             resource['display_name'] = user.display_name
         return resource
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return list(getUtility(IUserManager).users)
@@ -460,7 +458,6 @@ class OwnersForDomain(_UserBase):
             self._domain.remove_owner(email)
         return no_content(response)
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return list(self._domain.owners)
@@ -475,7 +472,6 @@ class ServerOwners(_UserBase):
         resource = self._make_collection(request)
         okay(response, etag(resource))
 
-    @paginate
     def _get_collection(self, request):
         """See `CollectionMixin`."""
         return list(getUtility(IUserManager).server_owners)



View it on GitLab: 
https://gitlab.com/mailman/mailman/commit/a5b6568179b9b17b2dd14b43bcccded15bf5025d
_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: 
https://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to