diff options
| author | Barry Warsaw | 2012-01-30 10:37:16 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2012-01-30 10:37:16 -0500 |
| commit | df6ec9f2960f1de89acc17ec28c3fe170a32e1dd (patch) | |
| tree | c212f56d3cb6510362b9f21c8dd625aec450bdd7 /src | |
| parent | 78b9ea398e6671d94f958e625b640383f1d43a75 (diff) | |
| download | mailman-df6ec9f2960f1de89acc17ec28c3fe170a32e1dd.tar.gz mailman-df6ec9f2960f1de89acc17ec28c3fe170a32e1dd.tar.zst mailman-df6ec9f2960f1de89acc17ec28c3fe170a32e1dd.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/app/docs/chains.rst | 4 | ||||
| -rw-r--r-- | src/mailman/app/moderator.py | 18 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_moderation.py | 14 | ||||
| -rw-r--r-- | src/mailman/bin/checkdbs.py | 9 | ||||
| -rw-r--r-- | src/mailman/config/configure.zcml | 15 | ||||
| -rw-r--r-- | src/mailman/docs/NEWS.rst | 13 | ||||
| -rw-r--r-- | src/mailman/interfaces/requests.py | 19 | ||||
| -rw-r--r-- | src/mailman/model/docs/requests.rst | 5 | ||||
| -rw-r--r-- | src/mailman/model/requests.py | 17 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_listmanager.py | 4 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_requests.py | 66 | ||||
| -rw-r--r-- | src/mailman/rest/docs/moderation.rst | 131 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 8 | ||||
| -rw-r--r-- | src/mailman/rest/moderation.py | 116 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_moderation.py | 107 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_users.py | 2 |
16 files changed, 493 insertions, 55 deletions
diff --git a/src/mailman/app/docs/chains.rst b/src/mailman/app/docs/chains.rst index 8a8ac0cc2..7096cc17c 100644 --- a/src/mailman/app/docs/chains.rst +++ b/src/mailman/app/docs/chains.rst @@ -226,8 +226,8 @@ first item is a type code and the second item is a message id. The message itself is held in the message store. :: - >>> from mailman.interfaces.requests import IRequests - >>> list_requests = getUtility(IRequests).get_list_requests(mlist) + >>> from mailman.interfaces.requests import IListRequests + >>> list_requests = IListRequests(mlist) >>> rkey, rdata = list_requests.get_request(data['id']) >>> from mailman.interfaces.messages import IMessageStore diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py index 11eccd495..3c3603619 100644 --- a/src/mailman/app/moderator.py +++ b/src/mailman/app/moderator.py @@ -49,7 +49,7 @@ from mailman.interfaces.listmanager import ListDeletingEvent from mailman.interfaces.member import ( AlreadySubscribedError, DeliveryMode, NotAMemberError) from mailman.interfaces.messages import IMessageStore -from mailman.interfaces.requests import IRequests, RequestType +from mailman.interfaces.requests import IListRequests, RequestType from mailman.utilities.datetime import now from mailman.utilities.i18n import make @@ -100,7 +100,7 @@ def hold_message(mlist, msg, msgdata=None, reason=None): msgdata['_mod_reason'] = reason msgdata['_mod_hold_date'] = now().isoformat() # Now hold this request. We'll use the message_id as the key. - requestsdb = getUtility(IRequests).get_list_requests(mlist) + requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.held_message, message_id, msgdata) return request_id @@ -110,14 +110,14 @@ def hold_message(mlist, msg, msgdata=None, reason=None): def handle_message(mlist, id, action, comment=None, preserve=False, forward=None): message_store = getUtility(IMessageStore) - requestdb = getUtility(IRequests).get_list_requests(mlist) + requestdb = IListRequests(mlist) key, msgdata = requestdb.get_request(id) # Handle the action. rejection = None message_id = msgdata['_mod_message_id'] sender = msgdata['_mod_sender'] subject = msgdata['_mod_subject'] - if action is Action.defer: + if action in (Action.defer, Action.hold): # Nothing to do, but preserve the message for later. preserve = True elif action is Action.discard: @@ -205,7 +205,7 @@ def hold_subscription(mlist, address, realname, password, mode, language): delivery_mode=str(mode), language=language) # Now hold this request. We'll use the address as the key. - requestsdb = getUtility(IRequests).get_list_requests(mlist) + requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.subscription, address, data) vlog.info('%s: held subscription request from %s', @@ -231,7 +231,7 @@ def hold_subscription(mlist, address, realname, password, mode, language): def handle_subscription(mlist, id, action, comment=None): - requestdb = getUtility(IRequests).get_list_requests(mlist) + requestdb = IListRequests(mlist) if action is Action.defer: # Nothing to do. return @@ -277,7 +277,7 @@ def handle_subscription(mlist, id, action, comment=None): def hold_unsubscription(mlist, address): data = dict(address=address) - requestsdb = getUtility(IRequests).get_list_requests(mlist) + requestsdb = IListRequests(mlist) request_id = requestsdb.hold_request( RequestType.unsubscription, address, data) vlog.info('%s: held unsubscription request from %s', @@ -303,7 +303,7 @@ def hold_unsubscription(mlist, address): def handle_unsubscription(mlist, id, action, comment=None): - requestdb = getUtility(IRequests).get_list_requests(mlist) + requestdb = IListRequests(mlist) key, data = requestdb.get_request(id) address = data['address'] if action is Action.defer: @@ -368,6 +368,6 @@ def handle_ListDeletingEvent(event): return # Get the held requests database for the mailing list. Since the mailing # list is about to get deleted, we can delete all associated requests. - requestsdb = getUtility(IRequests).get_list_requests(event.mailing_list) + requestsdb = IListRequests(event.mailing_list) for request in requestsdb.held_requests: requestsdb.delete_request(request.id) diff --git a/src/mailman/app/tests/test_moderation.py b/src/mailman/app/tests/test_moderation.py index 2a75043d5..262aa4480 100644 --- a/src/mailman/app/tests/test_moderation.py +++ b/src/mailman/app/tests/test_moderation.py @@ -29,6 +29,7 @@ import unittest from mailman.app.lifecycle import create_list from mailman.app.moderator import handle_message, hold_message from mailman.interfaces.action import Action +from mailman.interfaces.requests import IListRequests from mailman.runners.incoming import IncomingRunner from mailman.runners.outgoing import OutgoingRunner from mailman.runners.pipeline import PipelineRunner @@ -95,3 +96,16 @@ Message-ID: <alpha> # envelope. self.assertEqual(message['x-mailfrom'], 'test-bounces@example.com') self.assertEqual(message['x-rcptto'], 'bart@example.com') + + def test_hold_action_alias_for_defer(self): + # In handle_message(), the 'hold' action is the same as 'defer' for + # purposes of this API. + request_id = hold_message(self._mlist, self._msg) + handle_message(self._mlist, request_id, Action.defer) + # The message is still in the pending requests. + requests_db = IListRequests(self._mlist) + key, data = requests_db.get_request(request_id) + self.assertEqual(key, '<alpha>') + handle_message(self._mlist, request_id, Action.hold) + key, data = requests_db.get_request(request_id) + self.assertEqual(key, '<alpha>') diff --git a/src/mailman/bin/checkdbs.py b/src/mailman/bin/checkdbs.py index 023dddbda..42d5fc091 100644 --- a/src/mailman/bin/checkdbs.py +++ b/src/mailman/bin/checkdbs.py @@ -20,7 +20,6 @@ import time import optparse from email.Charset import Charset -from zope.component import getUtility from mailman import MailList from mailman import Utils @@ -29,7 +28,7 @@ from mailman.configuration import config from mailman.core.i18n import _ from mailman.email.message import UserNotification from mailman.initialize import initialize -from mailman.interfaces.requests import IRequests +from mailman.interfaces.requests import IListRequests, RequestType from mailman.version import MAILMAN_VERSION # Work around known problems with some RedHat cron daemons @@ -63,7 +62,7 @@ def pending_requests(mlist): lcset = mlist.preferred_language.charset pending = [] first = True - requestsdb = config.db.get_list_requests(mlist) + requestsdb = IListRequests(mlist) for request in requestsdb.of_type(RequestType.subscription): if first: pending.append(_('Pending subscriptions:')) @@ -128,7 +127,7 @@ def auto_discard(mlist): # Discard old held messages discard_count = 0 expire = config.days(mlist.max_days_to_hold) - requestsdb = config.db.get_list_requests(mlist) + requestsdb = IListRequests(mlist) heldmsgs = list(requestsdb.of_type(RequestType.held_message)) if expire and heldmsgs: for request in heldmsgs: @@ -158,7 +157,7 @@ def main(): # The list must be locked in order to open the requests database mlist = MailList.MailList(name) try: - count = getUtility(IRequests).get_list_requests(mlist).count + count = IListRequests(mlist).count # While we're at it, let's evict yesterday's autoresponse data midnight_today = midnight() evictions = [] diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index bc08eb8c5..aad79adaf 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -6,14 +6,20 @@ <adapter for="mailman.interfaces.mailinglist.IMailingList" - factory="mailman.model.autorespond.AutoResponseSet" provides="mailman.interfaces.autorespond.IAutoResponseSet" + factory="mailman.model.autorespond.AutoResponseSet" /> <adapter for="mailman.interfaces.mailinglist.IMailingList" - factory="mailman.model.mailinglist.AcceptableAliasSet" provides="mailman.interfaces.mailinglist.IAcceptableAliasSet" + factory="mailman.model.mailinglist.AcceptableAliasSet" + /> + + <adapter + for="mailman.interfaces.mailinglist.IMailingList" + provides="mailman.interfaces.requests.IListRequests" + factory="mailman.model.requests.ListRequests" /> <utility @@ -62,11 +68,6 @@ /> <utility - factory="mailman.model.requests.Requests" - provides="mailman.interfaces.requests.IRequests" - /> - - <utility factory="mailman.styles.manager.StyleManager" provides="mailman.interfaces.styles.IStyleManager" /> diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst index 4411166fd..f101cae06 100644 --- a/src/mailman/docs/NEWS.rst +++ b/src/mailman/docs/NEWS.rst @@ -21,6 +21,13 @@ Architecture * Dynamically calculate the `List-Id` header instead of storing it in the database. This means it cannot be changed. +REST +---- + * Held messages can now be moderated through the REST API. Mailing list + resources now accept a `held` path component. GETing this returns all held + messages for the mailing list. POSTing to a specific request id under this + url can dispose of the message using `Action` enums. + Interfaces ---------- * Add property `IUserManager.members` to return all `IMembers` in the system. @@ -28,6 +35,12 @@ Interfaces every mailing list as (list_name, mail_host). * Remove previously deprecated `IListManager.get_mailing_lists()`. * `IMailTransportAgentAliases` now explicitly accepts duck-typed arguments. + * `IRequests` interface is removed. Now just use adaptation from + `IListRequests` directly (which takes an `IMailingList` object). + * `handle_message()` now allows for `Action.hold` which is synonymous with + `Action.defer` (since the message is already being held). + * `IListRequests.get_request()` now takes an optional `request_type` + argument to narrow the search for the given request. Commands -------- diff --git a/src/mailman/interfaces/requests.py b/src/mailman/interfaces/requests.py index 55fb6395c..6e3162274 100644 --- a/src/mailman/interfaces/requests.py +++ b/src/mailman/interfaces/requests.py @@ -26,7 +26,6 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ 'IListRequests', - 'IRequests', 'RequestType', ] @@ -87,10 +86,14 @@ class IListRequests(Interface): Only items with a matching `type' are returned. """ - def get_request(request_id): + def get_request(request_id, request_type): """Get the data associated with the request id, or None. :param request_id: The unique id for the request. + :type request_id: int + :param request_type: Optional request type that the requested id must + match, otherwise no match is returned. + :type request_type: `RequestType` :return: A 2-tuple of the key and data originally held, or None if the `request_id` is not in the database. """ @@ -101,15 +104,3 @@ class IListRequests(Interface): :param request_id: The unique id for the request. :raises KeyError: If `request_id` is not in the database. """ - - - -class IRequests(Interface): - """The requests database.""" - - def get_list_requests(mailing_list): - """Return the `IListRequests` object for the given mailing list. - - :param mailing_list: An `IMailingList`. - :return: An `IListRequests` object for the mailing list. - """ diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst index 1bc66c40a..ab92917cd 100644 --- a/src/mailman/model/docs/requests.rst +++ b/src/mailman/model/docs/requests.rst @@ -37,12 +37,11 @@ A set of requests are always related to a particular mailing list, so given a mailing list you need to get its requests object. :: - >>> from mailman.interfaces.requests import IListRequests, IRequests - >>> from zope.component import getUtility + >>> from mailman.interfaces.requests import IListRequests >>> from zope.interface.verify import verifyObject >>> mlist = create_list('test@example.com') - >>> requests = getUtility(IRequests).get_list_requests(mlist) + >>> requests = IListRequests(mlist) >>> verifyObject(IListRequests, requests) True >>> requests.mailing_list diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py index bd7fe3796..4a3efa67f 100644 --- a/src/mailman/model/requests.py +++ b/src/mailman/model/requests.py @@ -15,13 +15,12 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. -"""Implementations of the IRequests and IListRequests interfaces.""" +"""Implementations of the pending requests interfaces.""" from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ - 'Requests', ] @@ -34,7 +33,7 @@ from mailman.config import config from mailman.database.model import Model from mailman.database.types import Enum from mailman.interfaces.pending import IPendable, IPendings -from mailman.interfaces.requests import IListRequests, IRequests, RequestType +from mailman.interfaces.requests import IListRequests, RequestType @@ -91,10 +90,12 @@ class ListRequests: config.db.store.add(request) return request.id - def get_request(self, request_id): + def get_request(self, request_id, request_type=None): result = config.db.store.get(_Request, request_id) if result is None: return None + if request_type is not None and result.request_type != request_type: + return None if result.data_hash is None: return result.key, result.data_hash pendable = getUtility(IPendings).confirm( @@ -113,14 +114,6 @@ class ListRequests: -class Requests: - implements(IRequests) - - def get_list_requests(self, mailing_list): - return ListRequests(mailing_list) - - - class _Request(Model): """Table for mailing list hold requests.""" diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py index 2022ce738..624f232d4 100644 --- a/src/mailman/model/tests/test_listmanager.py +++ b/src/mailman/model/tests/test_listmanager.py @@ -34,7 +34,7 @@ from mailman.interfaces.listmanager import ( IListManager, ListCreatedEvent, ListCreatingEvent, ListDeletedEvent, ListDeletingEvent) from mailman.interfaces.messages import IMessageStore -from mailman.interfaces.requests import IRequests +from mailman.interfaces.requests import IListRequests from mailman.interfaces.subscriptions import ISubscriptionService from mailman.interfaces.usermanager import IUserManager from mailman.testing.helpers import ( @@ -120,7 +120,7 @@ Message-ID: <argon> getUtility(IListManager).delete(self._ant) # This is a hack. ListRequests don't access self._mailinglist in # their get_request() method. - requestsdb = getUtility(IRequests).get_list_requests(None) + requestsdb = IListRequests(self._bee) request = requestsdb.get_request(request_id) self.assertEqual(request, None) saved_message = getUtility(IMessageStore).get_message_by_id('<argon>') diff --git a/src/mailman/model/tests/test_requests.py b/src/mailman/model/tests/test_requests.py new file mode 100644 index 000000000..a85add748 --- /dev/null +++ b/src/mailman/model/tests/test_requests.py @@ -0,0 +1,66 @@ +# Copyright (C) 2012 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/>. + +"""Test the various pending requests interfaces.""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + ] + + +import unittest + +from mailman.app.lifecycle import create_list +from mailman.app.moderator import hold_message +from mailman.interfaces.requests import IListRequests, RequestType +from mailman.testing.helpers import specialized_message_from_string as mfs +from mailman.testing.layers import ConfigLayer + + + +class TestRequests(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('ant@example.com') + self._msg = mfs("""\ +From: anne@example.com +To: ant@example.com +Subject: Something +Message-ID: <alpha> + +Something else. +""") + + def test_get_request_with_type(self): + # get_request() takes an optional request type. + request_id = hold_message(self._mlist, self._msg) + requests_db = IListRequests(self._mlist) + # Submit a request with a non-matching type. This should return None + # as if there were no matches. + response = requests_db.get_request( + request_id, RequestType.subscription) + self.assertEqual(response, None) + # Submit the same request with a matching type. + key, data = requests_db.get_request( + request_id, RequestType.held_message) + self.assertEqual(key, '<alpha>') + # It should also succeed with no optional request type given. + key, data = requests_db.get_request(request_id) + self.assertEqual(key, '<alpha>') diff --git a/src/mailman/rest/docs/moderation.rst b/src/mailman/rest/docs/moderation.rst new file mode 100644 index 000000000..44a2183ec --- /dev/null +++ b/src/mailman/rest/docs/moderation.rst @@ -0,0 +1,131 @@ +======================= +Held message moderation +======================= + +Held messages can be moderated through the REST API. A mailing list starts +out with no held messages. + + >>> ant = create_list('ant@example.com') + >>> transaction.commit() + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held') + http_etag: "..." + start: 0 + total_size: 0 + +When a message gets held for moderator approval, it shows up in this list. +:: + + >>> msg = message_from_string("""\ + ... From: anne@example.com + ... To: ant@example.com + ... Subject: Something + ... Message-ID: <alpha> + ... + ... Something else. + ... """) + + >>> from mailman.app.moderator import hold_message + >>> request_id = hold_message(ant, msg, {'extra': 7}, 'Because') + >>> transaction.commit() + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held') + entry 0: + data: {u'_mod_subject': u'Something', + u'_mod_message_id': u'<alpha>', + u'extra': 7, + u'_mod_fqdn_listname': u'ant@example.com', + u'_mod_hold_date': u'2005-08-01T07:49:23', + u'_mod_reason': u'Because', + u'_mod_sender': u'anne@example.com'} + http_etag: "..." + id: 1 + key: <alpha> + http_etag: "..." + start: 0 + total_size: 1 + +You can get an individual held message by providing the *request id* for that +message. This will include the text of the message. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1') + data: {u'_mod_subject': u'Something', + u'_mod_message_id': u'<alpha>', + u'extra': 7, + u'_mod_fqdn_listname': u'ant@example.com', + u'_mod_hold_date': u'2005-08-01T07:49:23', + u'_mod_reason': u'Because', + u'_mod_sender': u'anne@example.com'} + http_etag: "..." + id: 1 + key: <alpha> + msg: + From: anne@example.com + To: ant@example.com + Subject: Something + Message-ID: <alpha> + X-Message-ID-Hash: GCSMSG43GYWWVUMO6F7FBUSSPNXQCJ6M + <BLANKLINE> + Something else. + <BLANKLINE> + +Individual messages can be moderated through the API by POSTing back to the +held message's resource. The POST data requires an action of one of the +following: + + * discard - throw the message away. + * reject - bounces the message back to the original author. + * defer - defer any action on the message (continue to hold it) + * accept - accept the message for posting. + +Let's see what happens when the above message is deferred. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1', { + ... 'action': 'defer', + ... }) + content-length: 0 + date: ... + server: ... + status: 204 + +The message is still in the moderation queue. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1') + data: {u'_mod_subject': u'Something', + u'_mod_message_id': u'<alpha>', + u'extra': 7, + u'_mod_fqdn_listname': u'ant@example.com', + u'_mod_hold_date': u'2005-08-01T07:49:23', + u'_mod_reason': u'Because', + u'_mod_sender': u'anne@example.com'} + http_etag: "..." + id: 1 + key: <alpha> + msg: From: anne@example.com + To: ant@example.com + Subject: Something + Message-ID: <alpha> + X-Message-ID-Hash: GCSMSG43GYWWVUMO6F7FBUSSPNXQCJ6M + <BLANKLINE> + Something else. + <BLANKLINE> + +The held message can be discarded. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1', { + ... 'action': 'discard', + ... }) + content-length: 0 + date: ... + server: ... + status: 204 + +After which, the message is gone from the moderation queue. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/held/1') + Traceback (most recent call last): + ... + HTTPError: HTTP Error 404: 404 Not Found + +- Hold another message +- Show accept +- Show reject? - probably not as we're just into testing app.moderator diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 9d3865d00..0103022e7 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -42,6 +42,7 @@ from mailman.rest.configuration import ListConfiguration from mailman.rest.helpers import ( CollectionMixin, etag, no_content, path_to, restish_matcher) from mailman.rest.members import AMember, MemberCollection +from mailman.rest.moderation import HeldMessages from mailman.rest.validator import Validator @@ -166,6 +167,13 @@ class AList(_ListBase): return http.not_found() return ListConfiguration(self._mlist, attribute) + @resource.child() + def held(self, request, segments): + """Return a list of held messages for the mailign list.""" + if self._mlist is None: + return http.not_found() + return HeldMessages(self._mlist) + class AllLists(_ListBase): diff --git a/src/mailman/rest/moderation.py b/src/mailman/rest/moderation.py new file mode 100644 index 000000000..7075a75be --- /dev/null +++ b/src/mailman/rest/moderation.py @@ -0,0 +1,116 @@ +# Copyright (C) 2012 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/>. + +"""REST API for Message moderation.""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + 'HeldMessage', + 'HeldMessages', + ] + + +from restish import http, resource +from zope.component import getUtility + +from mailman.app.moderator import handle_message +from mailman.interfaces.action import Action +from mailman.interfaces.messages import IMessageStore +from mailman.interfaces.requests import IListRequests, RequestType +from mailman.rest.helpers import CollectionMixin, etag, no_content +from mailman.rest.validator import Validator, enum_validator + + + +class HeldMessage(resource.Resource, CollectionMixin): + """Resource for moderating a held message.""" + + def __init__(self, mlist, request_id): + self._mlist = mlist + self._request_id = request_id + + @resource.GET() + def details(self, request): + requests = IListRequests(self._mlist) + try: + request_id = int(self._request_id) + except ValueError: + return http.bad_request() + results = requests.get_request(request_id, RequestType.held_message) + if results is None: + return http.not_found() + key, data = results + msg = getUtility(IMessageStore).get_message_by_id(key) + resource = dict( + key=key, + data=data, + msg=msg.as_string(), + id=request_id, + ) + return http.ok([], etag(resource)) + + @resource.POST() + def moderate(self, request): + try: + validator = Validator(action=enum_validator(Action)) + arguments = validator(request) + except ValueError as error: + return http.bad_request([], str(error)) + requests = IListRequests(self._mlist) + try: + request_id = int(self._request_id) + except ValueError: + return http.bad_request() + results = requests.get_request(request_id, RequestType.held_message) + if results is None: + return http.not_found() + handle_message(self._mlist, request_id, **arguments) + return no_content() + + + +class HeldMessages(resource.Resource, CollectionMixin): + """Resource for messages held for moderation.""" + + def __init__(self, mlist): + self._mlist = mlist + + def _resource_as_dict(self, req): + """See `CollectionMixin`.""" + key, data = self._requests.get_request(req.id) + return dict( + key=key, + data=data, + id=req.id, + ) + + def _get_collection(self, request): + requests = IListRequests(self._mlist) + self._requests = requests + return list(requests.of_type(RequestType.held_message)) + + @resource.GET() + def requests(self, request): + """/lists/listname/held""" + resource = self._make_collection(request) + return http.ok([], etag(resource)) + + @resource.child('{id}') + def message(self, request, segments, **kw): + return HeldMessage(self._mlist, kw['id']) diff --git a/src/mailman/rest/tests/test_moderation.py b/src/mailman/rest/tests/test_moderation.py new file mode 100644 index 000000000..79b0c8b80 --- /dev/null +++ b/src/mailman/rest/tests/test_moderation.py @@ -0,0 +1,107 @@ +# Copyright (C) 2012 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/>. + +"""REST moderation tests.""" + +from __future__ import absolute_import, print_function, unicode_literals + +__metaclass__ = type +__all__ = [ + ] + + +import unittest + +from urllib2 import HTTPError + +from mailman.app.lifecycle import create_list +from mailman.app.moderator import hold_message, hold_subscription +from mailman.config import config +from mailman.interfaces.member import DeliveryMode +from mailman.testing.helpers import ( + call_api, specialized_message_from_string as mfs) +from mailman.testing.layers import RESTLayer + + + +class TestModeration(unittest.TestCase): + layer = RESTLayer + + def setUp(self): + self._mlist = create_list('ant@example.com') + self._msg = mfs("""\ +From: anne@example.com +To: ant@example.com +Subject: Something +Message-ID: <alpha> + +Something else. +""") + config.db.commit() + + def test_not_found(self): + # When a bogus mailing list is given, 404 should result. + try: + # For Python 2.6 + call_api('http://localhost:9001/3.0/lists/bee@example.com/held') + except HTTPError as exc: + self.assertEqual(exc.code, 404) + else: + raise AssertionError('Expected HTTPError') + + def test_bad_request_id(self): + # Bad request when request_id is not an integer. + try: + # For Python 2.6 + call_api( + 'http://localhost:9001/3.0/lists/ant@example.com/held/bogus') + except HTTPError as exc: + self.assertEqual(exc.code, 400) + else: + raise AssertionError('Expected HTTPError') + + def test_subscription_request_as_held_message(self): + # Provide the request id of a subscription request using the held + # message API returns a not-found even though the request id is + # in the database. + held_id = hold_message(self._mlist, self._msg) + subscribe_id = hold_subscription( + self._mlist, 'bperson@example.net', 'Bart Person', 'xyz', + DeliveryMode.regular, 'en') + config.db.store.commit() + url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{0}' + try: + call_api(url.format(subscribe_id)) + except HTTPError as exc: + self.assertEqual(exc.code, 404) + else: + raise AssertionError('Expected HTTPError') + # But using the held_id returns a valid response. + response, content = call_api(url.format(held_id)) + self.assertEqual(response['key'], '<alpha>') + + def test_bad_action(self): + # POSTing to a held message with a bad action. + held_id = hold_message(self._mlist, self._msg) + url = 'http://localhost:9001/3.0/lists/ant@example.com/held/{0}' + try: + call_api(url.format(held_id), {'action': 'bogus'}) + except HTTPError as exc: + self.assertEqual(exc.code, 400) + self.assertEqual(exc.msg, 'Cannot convert parameters: action') + else: + raise AssertionError('Expected HTTPError') diff --git a/src/mailman/rest/tests/test_users.py b/src/mailman/rest/tests/test_users.py index 3c1c40fd3..1630eb96a 100644 --- a/src/mailman/rest/tests/test_users.py +++ b/src/mailman/rest/tests/test_users.py @@ -17,7 +17,7 @@ """REST user tests.""" -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ |
