[prev in list] [next in list] [prev in thread] [next in thread]
List: mailman-cvs
Subject: [Mailman-checkins] [Git][mailman/mailman][release-3.0] Rework pagination to fix the 'start' and 'tot
From: Barry Warsaw <gitlab () mg ! gitlab ! com>
Date: 2015-11-06 20:28:08
Message-ID: 563d0d58504c9_7c3270699c99685 () worker80 ! cluster ! gitlab ! com ! mail
[Download RAW message or body]
[Attachment #2 (multipart/alternative)]
Barry Warsaw pushed to branch release-3.0 at mailman / Mailman
Commits:
2b2814bb by Aurélien Bompard at 2015-11-06T14:23:39Z
Rework pagination to fix the 'start' and 'total_size' values
- - - - -
11 changed files:
- src/mailman/docs/NEWS.rst
- 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/docs/NEWS.rst
=====================================
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -49,6 +49,8 @@ Bugs
* Added Trove classifiers to setup.py. (Closes: #152)
* Fix the processing of subscription confirmation messages when the mailing
list is set to confirm-then-moderate. (Closes #114)
+ * Fix pagination values `start` and `total_size` in the REST API. Given by
+ Aurélien Bompard. (Closes: #154)
3.0.0 -- "Show Don't Tell"
=====================================
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
@@ -20,6 +20,7 @@
__all__ = [
'BadRequest',
'ChildError',
+ 'CollectionMixin',
'GetterSetter',
'NotFound',
'bad_request',
@@ -112,31 +113,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."""
@@ -168,22 +144,39 @@ 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
=====================================
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, path_to)
+ etag, no_content, not_found, okay, path_to)
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=path_to('lists/{0}'.format(mlist.list_id)),
)
- @paginate
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IListManager))
@@ -230,7 +229,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
@@ -251,7 +249,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, path_to)
+ created, etag, no_content, not_found, okay, path_to)
from mailman.rest.preferences import Preferences, ReadOnlyPreferences
from mailman.rest.validator import (
Validator, enum_validator, subscriber_validator)
@@ -76,7 +76,6 @@ class _MemberBase(CollectionMixin):
response['user'] = path_to('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
@@ -29,7 +29,7 @@ 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, path_to)
+ path_to)
from mailman.rest.validator import Validator
from zope.component import getUtility
@@ -50,7 +50,6 @@ class _QueuesBase(CollectionMixin):
self_link=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'], 'bee@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'], 'fly@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('test@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)
@@ -137,7 +136,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)
@@ -448,7 +446,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)
View it on GitLab: https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e
[Attachment #5 (text/html)]
<html lang='en'>
<head>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type'>
<title>
GitLab
</title>
</meta>
</head>
<style>
img {
max-width: 100%;
height: auto;
}
p.details {
font-style:italic;
color:#777
}
.footer p {
font-size:small;
color:#777
}
pre.commit-message {
white-space: pre-wrap;
}
.file-stats a {
text-decoration: none;
}
.file-stats .new-file {
color: #090;
}
.file-stats .deleted-file {
color: #B00;
}
</style>
<body>
<div class='content'>
<h3>Barry Warsaw pushed to branch release-3.0 at <a \
href="https://gitlab.com/mailman/mailman">mailman / Mailman</a></h3> <h4>
Commits:
</h4>
<ul>
<li>
<strong><a href="https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e">2b2814bb</a></strong>
<div>
<span>by Aurélien Bompard</span>
<i>at 2015-11-06T14:23:39Z</i>
</div>
<pre class='commit-message'>Rework pagination to fix the 'start' and 'total_size' \
values</pre> </li>
</ul>
<h4>11 changed files:</h4>
<ul>
<li class='file-stats'>
<a href='#diff-0'>
src/mailman/docs/NEWS.rst
</a>
</li>
<li class='file-stats'>
<a href='#diff-1'>
src/mailman/rest/docs/lists.rst
</a>
</li>
<li class='file-stats'>
<a href='#diff-2'>
src/mailman/rest/docs/membership.rst
</a>
</li>
<li class='file-stats'>
<a href='#diff-3'>
src/mailman/rest/docs/users.rst
</a>
</li>
<li class='file-stats'>
<a href='#diff-4'>
src/mailman/rest/helpers.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-5'>
src/mailman/rest/lists.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-6'>
src/mailman/rest/members.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-7'>
src/mailman/rest/queues.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-8'>
src/mailman/rest/tests/test_lists.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-9'>
src/mailman/rest/tests/test_paginate.py
</a>
</li>
<li class='file-stats'>
<a href='#diff-10'>
src/mailman/rest/users.py
</a>
</li>
</ul>
<h4>Changes:</h4>
<li id='diff-0'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-0'>
<strong>
src/mailman/docs/NEWS.rst
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/docs/NEWS.rst </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/docs/NEWS.rst </span><span \
style="color: #aaaaaa">@@ -49,6 +49,8 @@ Bugs </span> * Added Trove classifiers to \
setup.py. (Closes: #152)
* Fix the processing of subscription confirmation messages when the mailing
list is set to confirm-then-moderate. (Closes #114)
<span style="color: #000000;background-color: #ddffdd">+ * Fix pagination values \
`start` and `total_size` in the REST API. Given by + Aurélien Bompard. (Closes: \
#154) </span>
3.0.0 -- "Show Don't Tell"
</code></pre>
<br>
</li>
<li id='diff-1'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-1'>
<strong>
src/mailman/rest/docs/lists.rst
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/docs/lists.rst </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/docs/lists.rst </span><span \
style="color: #aaaaaa">@@ -76,7 +76,7 @@ page. </span> volume: 1
http_etag: "..."
start: 0
<span style="color: #000000;background-color: #ffdddd">- total_size: 1
</span><span style="color: #000000;background-color: #ddffdd">+ total_size: 2
</span>
>>> dump_json('http://localhost:9001/3.0/domains/example.com/lists'
... '?count=1&page=2')
<span style="color: #aaaaaa">@@ -91,8 +91,8 @@ page.
</span> self_link: http://localhost:9001/3.0/lists/bird.example.com
volume: 1
http_etag: "..."
<span style="color: #000000;background-color: #ffdddd">- start: 0
- total_size: 1
</span><span style="color: #000000;background-color: #ddffdd">+ start: 1
+ total_size: 2
</span>
Creating lists via the API
</code></pre>
<br>
</li>
<li id='diff-2'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-2'>
<strong>
src/mailman/rest/docs/membership.rst
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/docs/membership.rst </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/docs/membership.rst \
</span><span style="color: #aaaaaa">@@ -267,7 +267,7 @@ page. </span> user: \
http://localhost:9001/3.0/users/3 http_etag: ...
start: 0
<span style="color: #000000;background-color: #ffdddd">- total_size: 1
</span><span style="color: #000000;background-color: #ddffdd">+ total_size: 2
</span>
This works with members of a single list as well as with all members.
<span style="color: #aaaaaa">@@ -285,7 +285,7 @@ This works with members of a single \
list as well as with all members. </span> user: \
http://localhost:9001/3.0/users/3 http_etag: ...
start: 0
<span style="color: #000000;background-color: #ffdddd">- total_size: 1
</span><span style="color: #000000;background-color: #ddffdd">+ total_size: 5
</span>
Owners and moderators
</code></pre>
<br>
</li>
<li id='diff-3'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-3'>
<strong>
src/mailman/rest/docs/users.rst
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/docs/users.rst </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/docs/users.rst </span><span \
style="color: #aaaaaa">@@ -84,7 +84,7 @@ page. </span> user_id: 1
http_etag: "..."
start: 0
<span style="color: #000000;background-color: #ffdddd">- total_size: 1
</span><span style="color: #000000;background-color: #ddffdd">+ total_size: 2
</span>
>>> dump_json('http://localhost:9001/3.0/users?count=1&page=2')
entry 0:
<span style="color: #aaaaaa">@@ -94,8 +94,8 @@ page.
</span> self_link: http://localhost:9001/3.0/users/2
user_id: 2
http_etag: "..."
<span style="color: #000000;background-color: #ffdddd">- start: 0
- total_size: 1
</span><span style="color: #000000;background-color: #ddffdd">+ start: 1
+ total_size: 2
</span>
Creating users
</code></pre>
<br>
</li>
<li id='diff-4'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-4'>
<strong>
src/mailman/rest/helpers.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/helpers.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/helpers.py </span><span \
style="color: #aaaaaa">@@ -20,6 +20,7 @@ </span> __all__ = [
'BadRequest',
'ChildError',
<span style="color: #000000;background-color: #ddffdd">+ 'CollectionMixin',
</span> 'GetterSetter',
'NotFound',
'bad_request',
<span style="color: #aaaaaa">@@ -112,31 +113,6 @@ def etag(resource):
</span> return json.dumps(resource, cls=ExtendedEncoder)
<span style="color: #000000;background-color: #ffdddd">-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
-
-
</span>
class CollectionMixin:
"""Mixin class for common collection-ish things."""
<span style="color: #aaaaaa">@@ -168,22 +144,39 @@ class CollectionMixin:
</span> """
raise NotImplementedError
<span style="color: #000000;background-color: #ddffdd">+ 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]
+
</span> def _make_collection(self, request):
"""Provide the collection to the REST layer."""
<span style="color: #000000;background-color: #ffdddd">- collection = \
self._get_collection(request)
- if len(collection) == 0:
- return dict(start=0, total_size=0)
- else:
</span><span style="color: #000000;background-color: #ddffdd">+ start, \
total_size, collection = self._paginate( + request, \
self._get_collection(request)) + result = dict(start=start, \
total_size=total_size) + if len(collection) != 0:
</span> 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
<span style="color: #000000;background-color: #ffdddd">- return dict(
- start=0,
- total_size=len(collection),
- entries=entries,
- )
</span><span style="color: #000000;background-color: #ddffdd">+ \
result['entries'] = entries + return result
</span>
</code></pre>
<br>
</li>
<li id='diff-5'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-5'>
<strong>
src/mailman/rest/lists.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/lists.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/lists.py </span><span \
style="color: #aaaaaa">@@ -40,7 +40,7 @@ from mailman.interfaces.subscriptions import \
ISubscriptionService </span> from mailman.rest.listconf import ListConfiguration
from mailman.rest.helpers import (
CollectionMixin, GetterSetter, NotFound, bad_request, child, created,
<span style="color: #000000;background-color: #ffdddd">- etag, no_content, \
not_found, okay, paginate, path_to) </span><span style="color: \
#000000;background-color: #ddffdd">+ etag, no_content, not_found, okay, path_to) \
</span> from mailman.rest.members import AMember, MemberCollection from \
mailman.rest.post_moderation import HeldMessages from mailman.rest.sub_moderation \
import SubscriptionRequests <span style="color: #aaaaaa">@@ -112,7 +112,6 @@ class \
_ListBase(CollectionMixin): </span> \
self_link=path_to('lists/{0}'.format(mlist.list_id)), )
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IListManager))
<span style="color: #aaaaaa">@@ -230,7 +229,6 @@ class \
MembersOfList(MemberCollection): </span> self._mlist = mailing_list
self._role = role
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
# Overrides _MemberBase._get_collection() because we only want to
<span style="color: #aaaaaa">@@ -251,7 +249,6 @@ class ListsForDomain(_ListBase):
</span> resource = self._make_collection(request)
okay(response, etag(resource))
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.mailing_lists)
</code></pre>
<br>
</li>
<li id='diff-6'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-6'>
<strong>
src/mailman/rest/members.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/members.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/members.py </span><span \
style="color: #aaaaaa">@@ -38,7 +38,7 @@ from mailman.interfaces.user import IUser, \
UnverifiedAddressError </span> from mailman.interfaces.usermanager import \
IUserManager from mailman.rest.helpers import (
CollectionMixin, NotFound, accepted, bad_request, child, conflict,
<span style="color: #000000;background-color: #ffdddd">- created, etag, \
no_content, not_found, okay, paginate, path_to) </span><span style="color: \
#000000;background-color: #ddffdd">+ created, etag, no_content, not_found, okay, \
path_to) </span> from mailman.rest.preferences import Preferences, \
ReadOnlyPreferences from mailman.rest.validator import (
Validator, enum_validator, subscriber_validator)
<span style="color: #aaaaaa">@@ -76,7 +76,6 @@ class _MemberBase(CollectionMixin):
</span> response['user'] = path_to('users/{}'.format(user.user_id.int))
return response
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(ISubscriptionService))
</code></pre>
<br>
</li>
<li id='diff-7'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-7'>
<strong>
src/mailman/rest/queues.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/queues.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/queues.py </span><span \
style="color: #aaaaaa">@@ -29,7 +29,7 @@ from mailman.app.inject import inject_text \
</span> from mailman.interfaces.listmanager import IListManager from \
mailman.rest.helpers import (
CollectionMixin, bad_request, created, etag, no_content, not_found, okay,
<span style="color: #000000;background-color: #ffdddd">- paginate, path_to)
</span><span style="color: #000000;background-color: #ddffdd">+ path_to)
</span> from mailman.rest.validator import Validator
from zope.component import getUtility
<span style="color: #aaaaaa">@@ -50,7 +50,6 @@ class _QueuesBase(CollectionMixin):
</span> self_link=path_to('queues/{}'.format(name)),
)
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
return sorted(config.switchboards)
</code></pre>
<br>
</li>
<li id='diff-8'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-8'>
<strong>
src/mailman/rest/tests/test_lists.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/tests/test_lists.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/tests/test_lists.py \
</span><span style="color: #aaaaaa">@@ -287,7 +287,7 @@ class \
TestListPagination(unittest.TestCase): </span> \
'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.
<span style="color: #000000;background-color: #ffdddd">- \
self.assertEqual(resource['total_size'], 1) </span><span style="color: \
#000000;background-color: #ddffdd">+ self.assertEqual(resource['total_size'], \
6) </span> self.assertEqual(resource['start'], 0)
self.assertEqual(len(resource['entries']), 1)
entry = resource['entries'][0]
<span style="color: #aaaaaa">@@ -298,8 +298,8 @@ class \
TestListPagination(unittest.TestCase): </span> \
'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.
<span style="color: #000000;background-color: #ffdddd">- \
self.assertEqual(resource['total_size'], 1)
- self.assertEqual(resource['start'], 0)
</span><span style="color: #000000;background-color: #ddffdd">+ \
self.assertEqual(resource['total_size'], 6) + \
self.assertEqual(resource['start'], 1) </span> \
self.assertEqual(len(resource['entries']), 1) entry = resource['entries'][0]
self.assertEqual(entry['fqdn_listname'], 'bee@example.com')
<span style="color: #aaaaaa">@@ -309,8 +309,8 @@ class \
TestListPagination(unittest.TestCase): </span> \
'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.
<span style="color: #000000;background-color: #ffdddd">- \
self.assertEqual(resource['total_size'], 1)
- self.assertEqual(resource['start'], 0)
</span><span style="color: #000000;background-color: #ddffdd">+ \
self.assertEqual(resource['total_size'], 6) + \
self.assertEqual(resource['start'], 5) </span> \
self.assertEqual(len(resource['entries']), 1) entry = resource['entries'][0]
self.assertEqual(entry['fqdn_listname'], 'fly@example.com')
<span style="color: #aaaaaa">@@ -337,6 +337,6 @@ class \
TestListPagination(unittest.TestCase): </span> \
'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.
<span style="color: #000000;background-color: #ffdddd">- \
self.assertEqual(resource['total_size'], 0)
- self.assertEqual(resource['start'], 0)
</span><span style="color: #000000;background-color: #ddffdd">+ \
self.assertEqual(resource['total_size'], 6) + \
self.assertEqual(resource['start'], 6) </span> self.assertNotIn('entries', \
resource) </code></pre>
<br>
</li>
<li id='diff-9'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-9'>
<strong>
src/mailman/rest/tests/test_paginate.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/tests/test_paginate.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/tests/test_paginate.py \
</span><span style="color: #aaaaaa">@@ -27,7 +27,7 @@ import unittest </span> from \
falcon import HTTPInvalidParam, Request from mailman.app.lifecycle import \
create_list from mailman.database.transaction import transaction
<span style="color: #000000;background-color: #ffdddd">-from mailman.rest.helpers \
import paginate </span><span style="color: #000000;background-color: #ddffdd">+from \
mailman.rest.helpers import CollectionMixin </span> from mailman.testing.layers \
import RESTLayer
<span style="color: #aaaaaa">@@ -51,79 +51,84 @@ class \
TestPaginateHelper(unittest.TestCase): </span> with transaction():
self._mlist = create_list('test@example.com')
<span style="color: #000000;background-color: #ddffdd">+ 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()
+
</span> def test_no_pagination(self):
# When there is no pagination params in the request, all 5 items in
# the collection are returned.
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
</span><span style="color: #000000;background-color: #ddffdd">+ resource = \
self._get_resource() </span> # Expect 5 items
<span style="color: #000000;background-color: #ffdddd">- page = \
get_collection(None, _FakeRequest())
- self.assertEqual(page, ['one', 'two', 'three', 'four', 'five'])
</span><span style="color: #000000;background-color: #ddffdd">+ 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'])
</span>
def test_valid_pagination_request_page_one(self):
# ?count=2&page=1 returns the first page, with two items in it.
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- page = get_collection(None, _FakeRequest(2, 1))
- self.assertEqual(page, ['one', 'two'])
</span><span style="color: #000000;background-color: #ddffdd">+ 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'])
</span>
def test_valid_pagination_request_page_two(self):
# ?count=2&page=2 returns the second page, where a page has two items
# in it.
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- page = get_collection(None, _FakeRequest(2, 2))
- self.assertEqual(page, ['three', 'four'])
</span><span style="color: #000000;background-color: #ddffdd">+ 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'])
</span>
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.
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- page = get_collection(None, _FakeRequest(2, 3))
- self.assertEqual(page, ['five'])
</span><span style="color: #000000;background-color: #ddffdd">+ 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'])
</span>
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.
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- page = get_collection(None, _FakeRequest(2, 4))
- self.assertEqual(page, [])
</span><span style="color: #000000;background-color: #ddffdd">+ 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)
</span>
def test_count_as_string_returns_bad_request(self):
# ?count=two&page=2 are not valid values, so a bad request occurs.
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return []
- self.assertRaises(HTTPInvalidParam, get_collection,
- None, _FakeRequest('two', 1))
</span><span style="color: #000000;background-color: #ddffdd">+ resource = \
self._get_resource() + self.assertRaises(HTTPInvalidParam, \
resource._make_collection, + _FakeRequest('two', 1))
</span>
def test_negative_count(self):
# ?count=-1&page=1
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- self.assertRaises(HTTPInvalidParam, get_collection,
- None, _FakeRequest(-1, 1))
</span><span style="color: #000000;background-color: #ddffdd">+ resource = \
self._get_resource() + self.assertRaises(HTTPInvalidParam, \
resource._make_collection, + _FakeRequest(-1, 1))
</span>
def test_negative_page(self):
# ?count=1&page=-1
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- self.assertRaises(HTTPInvalidParam, get_collection,
- None, _FakeRequest(1, -1))
</span><span style="color: #000000;background-color: #ddffdd">+ resource = \
self._get_resource() + self.assertRaises(HTTPInvalidParam, \
resource._make_collection, + _FakeRequest(1, -1))
</span>
def test_negative_page_and_count(self):
# ?count=1&page=-1
<span style="color: #000000;background-color: #ffdddd">- @paginate
- def get_collection(self, request):
- return ['one', 'two', 'three', 'four', 'five']
- self.assertRaises(HTTPInvalidParam, get_collection,
- None, _FakeRequest(-1, -1))
</span><span style="color: #000000;background-color: #ddffdd">+ resource = \
self._get_resource() + self.assertRaises(HTTPInvalidParam, \
resource._make_collection, + _FakeRequest(-1, -1))
</span></code></pre>
<br>
</li>
<li id='diff-10'>
<a href='https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e#diff-10'>
<strong>
src/mailman/rest/users.py
</strong>
</a>
<hr>
<pre class="highlight"><code><span style="color: #000000;background-color: \
#ffdddd">--- a/src/mailman/rest/users.py </span><span style="color: \
#000000;background-color: #ddffdd">+++ b/src/mailman/rest/users.py </span><span \
style="color: #aaaaaa">@@ -35,8 +35,7 @@ from mailman.interfaces.usermanager import \
IUserManager </span> from mailman.rest.addresses import UserAddresses
from mailman.rest.helpers import (
BadRequest, CollectionMixin, GetterSetter, NotFound, bad_request, child,
<span style="color: #000000;background-color: #ffdddd">- conflict, created, etag, \
forbidden, no_content, not_found, okay, paginate,
- path_to)
</span><span style="color: #000000;background-color: #ddffdd">+ conflict, created, \
etag, forbidden, no_content, not_found, okay, path_to) </span> from \
mailman.rest.preferences import Preferences from mailman.rest.validator import (
PatchValidator, Validator, list_of_strings_validator)
<span style="color: #aaaaaa">@@ -137,7 +136,6 @@ class _UserBase(CollectionMixin):
</span> resource['display_name'] = user.display_name
return resource
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(getUtility(IUserManager).users)
<span style="color: #aaaaaa">@@ -448,7 +446,6 @@ class OwnersForDomain(_UserBase):
</span> self._domain.remove_owner(email)
return no_content(response)
<span style="color: #000000;background-color: #ffdddd">- @paginate
</span> def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.owners)
</code></pre>
<br>
</li>
</div>
<div class='footer' style='margin-top: 10px;'>
<p>
—
<br>
<a href="https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e">View \
it on GitLab</a> <br>
You're receiving this email because of your account on <a \
href="https://gitlab.com/">gitlab.com</a>. If you'd like to receive fewer emails, you \
can adjust your notification settings. <script \
type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","action":{"@type":"ViewAction","name":"View \
Commit","url":"https://gitlab.com/mailman/mailman/commit/2b2814bbe7bff7377b859af5d5f5d2dd12121f6e"}}</script>
</p>
</div>
</body>
</html>
_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: https://mail.python.org/mailman/options/mailman-checkins/mailman-cvs%40progressive-comp.com
[prev in list] [next in list] [prev in thread] [next in thread]
Configure |
About |
News |
Add a list |
Sponsored by KoreLogic