diff options
| author | Barry Warsaw | 2013-11-27 13:50:20 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2013-11-27 13:50:20 -0500 |
| commit | fc347a34a65ebd0a249da52079d7aa60621b3eb2 (patch) | |
| tree | 47226186811d4eb1c24875c2c8ae593ea4be07eb | |
| parent | 177d3f81f4c786ad51083dfce6c4a5fd127693bd (diff) | |
| download | mailman-fc347a34a65ebd0a249da52079d7aa60621b3eb2.tar.gz mailman-fc347a34a65ebd0a249da52079d7aa60621b3eb2.tar.zst mailman-fc347a34a65ebd0a249da52079d7aa60621b3eb2.zip | |
| -rw-r--r-- | src/mailman/rest/configuration.py | 33 | ||||
| -rw-r--r-- | src/mailman/rest/docs/lists.rst | 58 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 69 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_lists.py | 83 |
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') |
