Commit e0b49799 authored by Florian Fuchs's avatar Florian Fuchs

REST API: Added pagination functionality to list, member and user collections.

This is related to LP: #1156529.
parent ccf27cb8
......@@ -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('[email protected]')
>>> transaction.commit()
>>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
... '?count=1&page=1')
entry 0:
display_name: Ant
fqdn_listname: [email protected]
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: [email protected]
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
==========================
......
......@@ -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/[email protected]/roster/member'
... '?count=1&page=1')
entry 0:
address: [email protected]
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: [email protected]
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
=====================
......
......@@ -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
==============
......
......@@ -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,44 @@ def etag(resource):
return json.dumps(resource, cls=ExtendedEncoder)
def paginate(default_count=None):
"""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 dec(function):
def wrapper(*args, **kwargs):
# args[0] is self.
# restish Request object is expected as second arg.
request = args[1]
try:
count = int(request.GET['count'])
page = int(request.GET['page'])
# Wrong parameter types or not GET attributer in GET request.
except (AttributeError, ValueError, TypeError):
return http.bad_request([], b'Invalid parameters')
# No count/page params: Use defaults.
except KeyError:
count = default_count
page = 1
# Set indices
list_start = 0
list_end = None
if count is not None:
list_start = int((page - 1) * count)
list_end = int(page * count)
return function(*args, **kwargs)[list_start:list_end]
return wrapper
return dec
class CollectionMixin:
"""Mixin class for common collection-ish things."""
......
......@@ -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)
......@@ -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))
......
# 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('[email protected]')
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 two 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 two 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 two 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 two items
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 two items
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 []
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)
response = get_collection(None, request)
self.assertEqual(response.status, '400 Bad Request')
......@@ -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)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment