summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2013-11-27 13:50:20 -0500
committerBarry Warsaw2013-11-27 13:50:20 -0500
commitfc347a34a65ebd0a249da52079d7aa60621b3eb2 (patch)
tree47226186811d4eb1c24875c2c8ae593ea4be07eb /src
parent177d3f81f4c786ad51083dfce6c4a5fd127693bd (diff)
downloadmailman-fc347a34a65ebd0a249da52079d7aa60621b3eb2.tar.gz
mailman-fc347a34a65ebd0a249da52079d7aa60621b3eb2.tar.zst
mailman-fc347a34a65ebd0a249da52079d7aa60621b3eb2.zip
Diffstat (limited to 'src')
-rw-r--r--src/mailman/rest/configuration.py33
-rw-r--r--src/mailman/rest/docs/lists.rst58
-rw-r--r--src/mailman/rest/lists.py69
-rw-r--r--src/mailman/rest/tests/test_lists.py83
4 files changed, 190 insertions, 53 deletions
diff --git a/src/mailman/rest/configuration.py b/src/mailman/rest/configuration.py
index 560aafbde..c726d8a81 100644
--- a/src/mailman/rest/configuration.py
+++ b/src/mailman/rest/configuration.py
@@ -26,7 +26,6 @@ __all__ = [
from lazr.config import as_boolean, as_timedelta
-from operator import attrgetter
from restish import http, resource
from mailman.config import config
@@ -35,8 +34,7 @@ from mailman.core.errors import (
from mailman.interfaces.action import Action
from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.autorespond import ResponseAction
-from mailman.interfaces.mailinglist import (
- IAcceptableAliasSet, IListArchiverSet, ReplyToMunging)
+from mailman.interfaces.mailinglist import IAcceptableAliasSet, ReplyToMunging
from mailman.rest.helpers import GetterSetter, PATCH, etag, no_content
from mailman.rest.validator import PatchValidator, Validator, enum_validator
@@ -68,25 +66,6 @@ class AcceptableAliases(GetterSetter):
-class ListArchivers(GetterSetter):
- """Resource for list-specific archivers."""
-
- def get(self, mlist, attribute):
- """Return the mailing list's acceptable aliases."""
- assert attribute == 'archivers', (
- 'Unexpected attribute: {0}'.format(attribute))
- archiver_set = IListArchiverSet(mlist)
- return sorted(archiver_set.archivers, key=attrgetter('name'))
-
- def put(self, mlist, attribute, value):
- assert attribute == 'archivers', (
- 'Unexpected attribute: {0}'.format(attribute))
- archiver_set = IListArchiverSet(mlist)
- for key, value in value.iteritems():
- archivers_set.set(key, value)
-
-
-
# Additional validators for converting from web request strings to internal
# data types. See below for details.
@@ -101,15 +80,6 @@ def list_of_unicode(values):
"""Turn a list of things into a list of unicodes."""
return [unicode(value) for value in values]
-def list_of_pairs_to_dict(pairs):
- dict = {}
- # If pairs has only one element then it is not a list but a string.
- if not isinstance(pairs, list):
- pairs = [pairs]
- for key_value in pairs:
- parts = key_value.split('|')
- dict[parts[0]] = parts[1]
- return dict
# This is the list of IMailingList attributes that are exposed through the
@@ -128,7 +98,6 @@ def list_of_pairs_to_dict(pairs):
ATTRIBUTES = dict(
acceptable_aliases=AcceptableAliases(list_of_unicode),
- archivers=ListArchivers(list_of_pairs_to_dict),
admin_immed_notify=GetterSetter(as_boolean),
admin_notify_mchanges=GetterSetter(as_boolean),
administrivia=GetterSetter(as_boolean),
diff --git a/src/mailman/rest/docs/lists.rst b/src/mailman/rest/docs/lists.rst
index 295e8c0b7..27503c1c1 100644
--- a/src/mailman/rest/docs/lists.rst
+++ b/src/mailman/rest/docs/lists.rst
@@ -230,3 +230,61 @@ The mailing list does not exist.
>>> print list_manager.get('ant@example.com')
None
+
+
+Managing mailing list archivers
+===============================
+
+The Mailman system has some site-wide enabled archivers, and each mailing list
+can enable or disable these archivers individually. This gives list owners
+control over where traffic to their list is archived. You can see which
+archivers are available, and whether they are enabled for this mailing list.
+::
+
+ >>> mlist = create_list('dog@example.com')
+ >>> transaction.commit()
+
+ >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
+ http_etag: "..."
+ mail-archive: True
+ mhonarc: True
+ prototype: True
+
+You can set all the archiver states by putting new state flags on the
+resource.
+::
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/lists/dog@example.com/archivers', {
+ ... 'mail-archive': False,
+ ... 'mhonarc': True,
+ ... 'prototype': False,
+ ... }, method='PUT')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
+ http_etag: "..."
+ mail-archive: False
+ mhonarc: True
+ prototype: False
+
+You can change the state of a subset of the list archivers.
+::
+
+ >>> dump_json(
+ ... 'http://localhost:9001/3.0/lists/dog@example.com/archivers', {
+ ... 'mhonarc': False,
+ ... }, method='PATCH')
+ content-length: 0
+ date: ...
+ server: ...
+ status: 204
+
+ >>> dump_json('http://localhost:9001/3.0/lists/dog@example.com/archivers')
+ http_etag: "..."
+ mail-archive: False
+ mhonarc: False
+ prototype: False
diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py
index 32e22a76b..b8e754647 100644
--- a/src/mailman/rest/lists.py
+++ b/src/mailman/rest/lists.py
@@ -23,11 +23,13 @@ __metaclass__ = type
__all__ = [
'AList',
'AllLists',
+ 'ListArchivers',
'ListConfiguration',
'ListsForDomain',
]
+from lazr.config import as_boolean
from operator import attrgetter
from restish import http, resource
from zope.component import getUtility
@@ -36,11 +38,13 @@ from mailman.app.lifecycle import create_list, remove_list
from mailman.interfaces.domain import BadDomainSpecificationError
from mailman.interfaces.listmanager import (
IListManager, ListAlreadyExistsError)
+from mailman.interfaces.mailinglist import IListArchiverSet
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, paginate, path_to, restish_matcher)
+ CollectionMixin, GetterSetter, PATCH, etag, no_content, paginate, path_to,
+ restish_matcher)
from mailman.rest.members import AMember, MemberCollection
from mailman.rest.moderation import HeldMessages, SubscriptionRequests
from mailman.rest.validator import Validator
@@ -189,6 +193,13 @@ class AList(_ListBase):
return http.not_found()
return SubscriptionRequests(self._mlist)
+ @resource.child()
+ def archivers(self, request, segments):
+ """Return a representation of mailing list archivers."""
+ if self._mlist is None:
+ return http.not_found()
+ return ListArchivers(self._mlist)
+
class AllLists(_ListBase):
@@ -256,3 +267,59 @@ class ListsForDomain(_ListBase):
def _get_collection(self, request):
"""See `CollectionMixin`."""
return list(self._domain.mailing_lists)
+
+
+
+class ArchiverGetterSetter(GetterSetter):
+ """Resource for updating archiver statuses."""
+
+ def __init__(self, mlist):
+ super(ArchiverGetterSetter, self).__init__()
+ self._archiver_set = IListArchiverSet(mlist)
+
+ def put(self, mlist, attribute, value):
+ # attribute will contain the (bytes) name of the archiver that is
+ # getting a new status. value will be the representation of the new
+ # boolean status.
+ archiver = self._archiver_set.get(attribute.decode('utf-8'))
+ if archiver is None:
+ raise ValueError('No such archiver: {}'.format(attribute))
+ archiver.is_enabled = as_boolean(value)
+
+
+class ListArchivers(resource.Resource):
+ """The archivers for a list, with their enabled flags."""
+
+ def __init__(self, mlist):
+ self._mlist = mlist
+
+ @resource.GET()
+ def statuses(self, request):
+ """Get all the archiver statuses."""
+ archiver_set = IListArchiverSet(self._mlist)
+ resource = {archiver.name: archiver.is_enabled
+ for archiver in archiver_set.archivers}
+ return http.ok([], etag(resource))
+
+ def patch_put(self, request, is_optional):
+ archiver_set = IListArchiverSet(self._mlist)
+ kws = {archiver.name: ArchiverGetterSetter(self._mlist)
+ for archiver in archiver_set.archivers}
+ if is_optional:
+ # For a PUT, all attributes are optional.
+ kws['_optional'] = kws.keys()
+ try:
+ Validator(**kws).update(self._mlist, request)
+ except ValueError as error:
+ return http.bad_request([], str(error))
+ return no_content()
+
+ @resource.PUT()
+ def put_statuses(self, request):
+ """Update all the archiver statuses."""
+ return self.patch_put(request, is_optional=False)
+
+ @PATCH()
+ def patch_statuses(self, request):
+ """Patch some archiver statueses."""
+ return self.patch_put(request, is_optional=True)
diff --git a/src/mailman/rest/tests/test_lists.py b/src/mailman/rest/tests/test_lists.py
index 1c11865e6..77b85895b 100644
--- a/src/mailman/rest/tests/test_lists.py
+++ b/src/mailman/rest/tests/test_lists.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'TestListArchivers',
'TestLists',
'TestListsMissing',
]
@@ -28,16 +29,12 @@ __all__ = [
import unittest
-from zope.component import getUtility
from urllib2 import HTTPError
from zope.component import getUtility
from mailman.app.lifecycle import create_list
-from mailman.config import config
from mailman.database.transaction import transaction
from mailman.interfaces.usermanager import IUserManager
-from mailman.interfaces.listmanager import IListManager
-from mailman.model.mailinglist import ListArchiverSet
from mailman.testing.helpers import call_api
from mailman.testing.layers import RESTLayer
@@ -164,24 +161,70 @@ class TestLists(unittest.TestCase):
method='DELETE')
self.assertEqual(cm.exception.code, 404)
- def test_prototype_in_list_archivers(self):
- resource, response = call_api(
- 'http://localhost:9001/3.0/lists/test@example.com/config')
- self.assertEqual(response.status, 200)
- self.assertEqual(resource['archivers']['prototype'], 0)
- def test_lazy_add_archivers(self):
- call_api('http://localhost:9001/3.0/lists', {
- 'fqdn_listname': 'new_list@example.com',
- })
+
+class TestListArchivers(unittest.TestCase):
+ """Test corner cases for list archivers."""
+
+ layer = RESTLayer
+
+ def setUp(self):
+ with transaction():
+ self._mlist = create_list('ant@example.com')
+
+ def test_archiver_statuses(self):
resource, response = call_api(
- 'http://localhost:9001/3.0/lists/new_list@example.com/config')
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers')
self.assertEqual(response.status, 200)
- self.assertEqual(resource['archivers']['prototype'], 0)
+ # Remove the variable data.
+ resource.pop('http_etag')
+ self.assertEqual(resource, {
+ 'mail-archive': True,
+ 'mhonarc': True,
+ 'prototype': True,
+ })
+
+ def test_archiver_statuses_on_missing_lists(self):
+ # You cannot get the archiver statuses on a list that doesn't exist.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/bee.example.com/archivers')
+ self.assertEqual(cm.exception.code, 404)
+
+ def test_patch_status_on_bogus_archiver(self):
+ # You cannot set the status on an archiver the list doesn't know about.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers', {
+ 'bogus-archiver': True,
+ },
+ method='PATCH')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason,
+ 'Unexpected parameters: bogus-archiver')
- def test_set_archiver_enabled(self):
- mlist = getUtility(IListManager).create('newest_list@example.com')
- lset = ListArchiverSet(mlist)
- lset.set('prototype', 1)
- self.assertEqual(lset.isEnabled('prototype'), 1)
+ def test_put_incomplete_statuses(self):
+ # PUT requires the full resource representation. This one forgets to
+ # specify the prototype and mhonarc archiver.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers', {
+ 'mail-archive': True,
+ },
+ method='PUT')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason,
+ 'Missing parameters: mhonarc, prototype')
+ def test_patch_bogus_status(self):
+ # Archiver statuses must be interpretable as booleans.
+ with self.assertRaises(HTTPError) as cm:
+ call_api(
+ 'http://localhost:9001/3.0/lists/ant.example.com/archivers', {
+ 'mail-archive': 'sure',
+ 'mhonarc': False,
+ 'prototype': 'no'
+ },
+ method='PATCH')
+ self.assertEqual(cm.exception.code, 400)
+ self.assertEqual(cm.exception.reason, 'Invalid boolean value: sure')