summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2013-03-21 09:52:31 -0700
committerBarry Warsaw2013-03-21 09:52:31 -0700
commit2b9b4feff3845cd1eb6799037d8516510608e1a0 (patch)
tree0cb38a04f433595129f0226b3d15a42fd0263185 /src
parentbc4776ba20441810f2ff22d8436f450d4be2b439 (diff)
parentc8c7c3e95088db4b6e9e8b7d58094fe2818b622b (diff)
downloadmailman-2b9b4feff3845cd1eb6799037d8516510608e1a0.tar.gz
mailman-2b9b4feff3845cd1eb6799037d8516510608e1a0.tar.zst
mailman-2b9b4feff3845cd1eb6799037d8516510608e1a0.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/rest/docs/lists.rst43
-rw-r--r--src/mailman/rest/docs/membership.rst38
-rw-r--r--src/mailman/rest/docs/users.rst28
-rw-r--r--src/mailman/rest/helpers.py41
-rw-r--r--src/mailman/rest/lists.py5
-rw-r--r--src/mailman/rest/members.py3
-rw-r--r--src/mailman/rest/tests/test_paginate.py129
-rw-r--r--src/mailman/rest/users.py3
8 files changed, 287 insertions, 3 deletions
diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst
index 5c65a7951..ce113c9f1 100644
--- a/src/mailman/rest/docs/lists.rst
+++ b/src/mailman/rest/docs/lists.rst
@@ -50,6 +50,49 @@ You can also query for lists from a particular domain.
total_size: 1
+Paginating over list records
+----------------------------
+
+List records, as well as user and member records can be requested in page
+chunks that are defined with the GET params ``count`` and ``page``, with
+``count`` defining the length of the collection to be returned.
+
+ >>> mlist = create_list('bird@example.com')
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
+ ... '?count=1&page=1')
+ entry 0:
+ display_name: Ant
+ fqdn_listname: ant@example.com
+ http_etag: "..."
+ list_id: ant.example.com
+ list_name: ant
+ mail_host: example.com
+ member_count: 0
+ self_link: http://localhost:9001/3.0/lists/ant.example.com
+ volume: 1
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+ >>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
+ ... '?count=1&page=2')
+ entry 0:
+ display_name: Bird
+ fqdn_listname: bird@example.com
+ http_etag: "..."
+ list_id: bird.example.com
+ list_name: bird
+ mail_host: example.com
+ member_count: 0
+ self_link: http://localhost:9001/3.0/lists/bird.example.com
+ volume: 1
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+
Creating lists via the API
==========================
diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst
index 6189990cb..7f0ffae9d 100644
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -203,6 +203,44 @@ We can also get just the members of a single mailing list.
total_size: 2
+Paginating over member records
+------------------------------
+
+Instead of returning all member records at once, it's possible to return
+the as pages.
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/lists/ant@example.com/roster/member'
+ ... '?count=1&page=1')
+ entry 0:
+ address: aperson@example.com
+ delivery_mode: regular
+ http_etag: ...
+ list_id: ant.example.com
+ role: member
+ self_link: http://localhost:9001/3.0/members/4
+ user: http://localhost:9001/3.0/users/3
+ http_etag: ...
+ start: 0
+ total_size: 1
+
+This works with members of a single list as well as with all members.
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/members?count=1&page=1')
+ entry 0:
+ address: aperson@example.com
+ delivery_mode: regular
+ http_etag: ...
+ list_id: ant.example.com
+ role: member
+ self_link: http://localhost:9001/3.0/members/4
+ user: http://localhost:9001/3.0/users/3
+ http_etag: ...
+ start: 0
+ total_size: 1
+
+
Owners and moderators
=====================
diff --git a/src/mailman/rest/docs/users.rst b/src/mailman/rest/docs/users.rst
index 36ec28efc..93ae30d48 100644
--- a/src/mailman/rest/docs/users.rst
+++ b/src/mailman/rest/docs/users.rst
@@ -62,6 +62,34 @@ returned in the REST API.
total_size: 2
+Paginating over user records
+----------------------------
+
+It's possible to return pagable chunks of all user records by adding
+the GET params ``count`` and ``page`` to the request URI.
+
+ >>> dump_json('http://localhost:9001/3.0/users?count=1&page=1')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ display_name: Anne Person
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/users/1
+ user_id: 1
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+ >>> dump_json('http://localhost:9001/3.0/users?count=1&page=2')
+ entry 0:
+ created_on: 2005-08-01T07:49:23
+ http_etag: "..."
+ self_link: http://localhost:9001/3.0/users/2
+ user_id: 2
+ http_etag: "..."
+ start: 0
+ total_size: 1
+
+
Creating users
==============
diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py
index 197e74362..3b64dc0dc 100644
--- a/src/mailman/rest/helpers.py
+++ b/src/mailman/rest/helpers.py
@@ -38,8 +38,10 @@ from cStringIO import StringIO
from datetime import datetime, timedelta
from flufl.enum import Enum
from lazr.config import as_boolean
+from restish import http
from restish.http import Response
from restish.resource import MethodDecorator
+from urllib2 import HTTPError
from webob.multidict import MultiDict
from mailman.config import config
@@ -105,6 +107,45 @@ 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 either
+ in the request itself or by the ``default_count`` argument.
+ ``default_count=None`` will return the whole collection if the request
+ contains no count/page parameters.
+
+ :param default_count: The default page length if no count is specified.
+ :type default_count: int
+ :returns: Decorator function.
+ """
+ def wrapper(*args, **kwargs):
+ # args[0] is self.
+ # restish Request object is expected to be the second arg.
+ request = args[1]
+ # get the result
+ result = method(*args, **kwargs)
+ try:
+ count = int(request.GET['count'])
+ page = int(request.GET['page'])
+ # Set indices
+ list_start = 0
+ list_end = None
+ # slice list only if count is not None
+ if count is not None:
+ list_start = int((page - 1) * count)
+ list_end = int(page * count)
+ return result[list_start:list_end]
+ # Wrong parameter types or no GET attribute in request object.
+ except (AttributeError, ValueError, TypeError):
+ return http.bad_request([], b'Invalid parameters')
+ # No count/page params
+ except KeyError:
+ pass
+ return result
+ return wrapper
+
+
class CollectionMixin:
"""Mixin class for common collection-ish things."""
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index cefd91b7c..328472794 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -40,7 +40,7 @@ from mailman.interfaces.member import MemberRole
from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.rest.configuration import ListConfiguration
from mailman.rest.helpers import (
- CollectionMixin, etag, no_content, path_to, restish_matcher)
+ CollectionMixin, etag, no_content, path_to, restish_matcher, paginate)
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.moderation import HeldMessages, SubscriptionRequests
from mailman.rest.validator import Validator
@@ -115,6 +115,7 @@ class _ListBase(resource.Resource, CollectionMixin):
self_link=path_to('lists/{0}'.format(mlist.list_id)),
)
+ @paginate
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IListManager))
@@ -229,6 +230,7 @@ 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
@@ -250,6 +252,7 @@ class ListsForDomain(_ListBase):
resource = self._make_collection(request)
return http.ok([], etag(resource))
+ @paginate
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.mailing_lists)
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index 5cf357579..bf2cdf82b 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -43,7 +43,7 @@ from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.helpers import (
- CollectionMixin, PATCH, etag, no_content, path_to)
+ CollectionMixin, PATCH, etag, no_content, path_to, paginate)
from mailman.rest.preferences import Preferences, ReadOnlyPreferences
from mailman.rest.validator import (
Validator, enum_validator, subscriber_validator)
@@ -69,6 +69,7 @@ class _MemberBase(resource.Resource, CollectionMixin):
delivery_mode=member.delivery_mode,
)
+ @paginate
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(ISubscriptionService))
diff --git a/src/mailman/rest/tests/test_paginate.py b/src/mailman/rest/tests/test_paginate.py
new file mode 100644
index 000000000..ae73125b5
--- /dev/null
+++ b/src/mailman/rest/tests/test_paginate.py
@@ -0,0 +1,129 @@
+# Copyright (C) 2011-2013 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""paginate helper tests."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'TestPaginateHelper',
+ ]
+
+
+import unittest
+
+from urllib2 import HTTPError
+from zope.component import getUtility
+
+from mailman.app.lifecycle import create_list
+from mailman.database.transaction import transaction
+from mailman.interfaces.listmanager import IListManager
+from mailman.rest.helpers import paginate
+from mailman.testing.helpers import call_api
+from mailman.testing.layers import RESTLayer
+
+
+class FakeRequest:
+ """Fake restish.http.Request object."""
+
+ def __init__(self, count=None, page=None):
+ self.GET = {}
+ if count is not None:
+ self.GET['count'] = count
+ if page is not None:
+ self.GET['page'] = page
+
+
+class TestPaginateHelper(unittest.TestCase):
+ layer = RESTLayer
+
+ def setUp(self):
+ with transaction():
+ self._mlist = create_list('test@example.com')
+
+ def test_no_pagination(self):
+ # No pagination params in request
+ # Collection with 5 items.
+ @paginate
+ def get_collection(self, request):
+ return ['one', 'two', 'three', 'four', 'five']
+ # Expect 5 items
+ page = get_collection(None, FakeRequest())
+ self.assertEqual(page, ['one', 'two', 'three', 'four', 'five'])
+
+ def test_valid_pagination_request_page_one(self):
+ # ?count=2&page=1 is a valid GET query string.
+ # Collection with 5 items.
+ @paginate
+ def get_collection(self, request):
+ return ['one', 'two', 'three', 'four', 'five']
+ # Expect 2 items
+ page = get_collection(None, FakeRequest(2, 1))
+ self.assertEqual(page, ['one', 'two'])
+
+ def test_valid_pagination_request_page_two(self):
+ # ?count=2&page=2 is a valid GET query string.
+ # Collection with 5 items.
+ @paginate
+ def get_collection(self, request):
+ return ['one', 'two', 'three', 'four', 'five']
+ # Expect 2 items
+ page = get_collection(None, FakeRequest(2, 2))
+ self.assertEqual(page, ['three', 'four'])
+
+ def test_2nd_index_larger_than_total(self):
+ # ?count=2&page=3 is a valid GET query string.
+ # Collection with 5 items.
+ @paginate
+ def get_collection(self, request):
+ return ['one', 'two', 'three', 'four', 'five']
+ # Expect last item
+ page = get_collection(None, FakeRequest(2, 3))
+ self.assertEqual(page, ['five'])
+
+ def test_out_of_range_returns_empty_list(self):
+ # ?count=2&page=3 is a valid GET query string.
+ # Collection with 5 items.
+ @paginate
+ def get_collection(self, request):
+ return ['one', 'two', 'three', 'four', 'five']
+ # Expect empty list
+ page = get_collection(None, FakeRequest(2, 4))
+ self.assertEqual(page, [])
+
+ def test_count_as_string_returns_bad_request(self):
+ # ?count=two&page=2 are not valid values.
+ @paginate
+ def get_collection(self, request):
+ return []
+ # Expect Bad Request
+ response = get_collection(None, FakeRequest('two', 1))
+ self.assertEqual(response.status, '400 Bad Request')
+
+ def test_no_get_attr_returns_bad_request(self):
+ # ?count=two&page=2 are not valid values.
+ @paginate
+ def get_collection(self, request):
+ return []
+ request = FakeRequest()
+ del request.GET
+ # Assert request obj has no GET attr.
+ self.assertTrue(getattr(request, 'GET', None) is None)
+ # Expect Bad Request
+ response = get_collection(None, request)
+ self.assertEqual(response.status, '400 Bad Request')
diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py
index ffe2009b6..21bcad202 100644
--- a/src/mailman/rest/users.py
+++ b/src/mailman/rest/users.py
@@ -38,7 +38,7 @@ from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.rest.addresses import UserAddresses
from mailman.rest.helpers import (
- CollectionMixin, GetterSetter, PATCH, etag, no_content, path_to)
+ CollectionMixin, GetterSetter, PATCH, etag, no_content, path_to, paginate)
from mailman.rest.preferences import Preferences
from mailman.rest.validator import PatchValidator, Validator
@@ -86,6 +86,7 @@ class _UserBase(resource.Resource, CollectionMixin):
resource['display_name'] = user.display_name
return resource
+ @paginate
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IUserManager).users)