From 0128cd2b2ec3da45dd7636b8843cb4bd3e1fff73 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 15 Dec 2012 15:44:05 -0500 Subject: [PATCH] Expose held subscription/unsubscription requests via the API. * hold_subscription(): Don't str(mode) to get a string representation, just mode.name since we know it is a DeliveryMode. This means we don't need to split the value later in handle_subscription(). --- src/mailman/app/moderator.py | 5 +- src/mailman/model/requests.py | 3 ++ src/mailman/rest/docs/moderation.rst | 94 ++++++++++++++++++++++++++++++++++-- src/mailman/rest/lists.py | 11 ++++- src/mailman/rest/moderation.py | 55 +++++++++++++++++++-- 5 files changed, 156 insertions(+), 12 deletions(-) diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py index 2e2711809..7e6f4758e 100644 --- a/src/mailman/app/moderator.py +++ b/src/mailman/app/moderator.py @@ -202,7 +202,7 @@ def hold_subscription(mlist, address, display_name, password, mode, language): address=address, display_name=display_name, password=password, - delivery_mode=str(mode), + delivery_mode=mode.name, language=language) # Now hold this request. We'll use the address as the key. requestsdb = IListRequests(mlist) @@ -246,8 +246,7 @@ def handle_subscription(mlist, id, action, comment=None): lang=getUtility(ILanguageManager)[data['language']]) elif action is Action.accept: key, data = requestdb.get_request(id) - enum_value = data['delivery_mode'].split('.')[-1] - delivery_mode = DeliveryMode(enum_value) + delivery_mode = DeliveryMode(data['delivery_mode']) address = data['address'] display_name = data['display_name'] language = getUtility(ILanguageManager)[data['language']] diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py index 5eb940233..9de5df8b3 100644 --- a/src/mailman/model/requests.py +++ b/src/mailman/model/requests.py @@ -40,6 +40,8 @@ from mailman.interfaces.requests import IListRequests, RequestType @implementer(IPendable) class DataPendable(dict): + """See `IPendable`.""" + def update(self, mapping): # Keys and values must be strings (unicodes, but bytes values are # accepted for now). Any other types for keys are a programming @@ -58,6 +60,7 @@ class DataPendable(dict): @implementer(IListRequests) class ListRequests: + """See `IListRequests`.""" def __init__(self, mailing_list): self.mailing_list = mailing_list diff --git a/src/mailman/rest/docs/moderation.rst b/src/mailman/rest/docs/moderation.rst index e4c298dc4..6883b5061 100644 --- a/src/mailman/rest/docs/moderation.rst +++ b/src/mailman/rest/docs/moderation.rst @@ -1,9 +1,18 @@ -======================= -Held message moderation -======================= +========== +Moderation +========== + +There are two kinds of moderation tasks a list administrator may need to +perform. Messages which are held for approval can be accepted, rejected, +discarded, or deferred. Subscription (and sometimes unsubscription) requests +can similarly be accepted, discarded, rejected, or deferred. + + +Message moderation +================== Held messages can be moderated through the REST API. A mailing list starts -out with no held messages. +with no held messages. >>> ant = create_list('ant@example.com') >>> transaction.commit() @@ -186,3 +195,80 @@ to the original author. 1 >>> print messages[0].msg['subject'] Request to mailing list "Ant" rejected + + +Subscription moderation +======================= + +Subscription and unsubscription requests can be moderated via the REST API as +well. A mailing list starts with no pending subscription or unsubscription +requests. + + >>> ant.admin_immed_notify = False + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/requests') + http_etag: "..." + start: 0 + total_size: 0 + +When Anne tries to subscribe to the Ant list, her subscription is held for +moderator approval. + + >>> from mailman.app.moderator import hold_subscription + >>> from mailman.interfaces.member import DeliveryMode + >>> hold_subscription( + ... ant, 'anne@example.com', 'Anne Person', + ... 'password', DeliveryMode.regular, 'en') + 1 + >>> transaction.commit() + +The subscription request is available from the mailing list. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/requests') + entry 0: + address: anne@example.com + delivery_mode: regular + display_name: Anne Person + http_etag: "..." + id: 1 + key: anne@example.com + language: en + password: password + type: subscription + when: 2005-08-01T07:49:23 + http_etag: "..." + start: 0 + total_size: 1 + +Bart tries to leave a mailing list, but he may not be allowed to. + + >>> from mailman.app.membership import add_member + >>> from mailman.app.moderator import hold_unsubscription + >>> bart = add_member(ant, 'bart@example.com', 'Bart Person', + ... 'password', DeliveryMode.regular, 'en') + >>> hold_unsubscription(ant, 'bart@example.com') + 2 + >>> transaction.commit() + +The unsubscription request is also available from the mailing list. + + >>> dump_json('http://localhost:9001/3.0/lists/ant@example.com/requests') + entry 0: + address: anne@example.com + delivery_mode: regular + display_name: Anne Person + http_etag: "..." + id: 1 + key: anne@example.com + language: en + password: password + type: subscription + when: 2005-08-01T07:49:23 + entry 1: + address: bart@example.com + http_etag: "..." + id: 2 + key: bart@example.com + type: unsubscription + http_etag: "..." + start: 0 + total_size: 2 diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index 4e9de6905..4a4b243b3 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -42,7 +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.moderation import HeldMessages, SubscriptionRequests from mailman.rest.validator import Validator @@ -176,11 +176,18 @@ class AList(_ListBase): @resource.child() def held(self, request, segments): - """Return a list of held messages for the mailign list.""" + """Return a list of held messages for the mailing list.""" if self._mlist is None: return http.not_found() return HeldMessages(self._mlist) + @resource.child() + def requests(self, request, segments): + """Return a list of subscription/unsubscription requests.""" + if self._mlist is None: + return http.not_found() + return SubscriptionRequests(self._mlist) + class AllLists(_ListBase): diff --git a/src/mailman/rest/moderation.py b/src/mailman/rest/moderation.py index 7075a75be..693216aba 100644 --- a/src/mailman/rest/moderation.py +++ b/src/mailman/rest/moderation.py @@ -23,6 +23,7 @@ __metaclass__ = type __all__ = [ 'HeldMessage', 'HeldMessages', + 'SubscriptionRequests', ] @@ -59,6 +60,8 @@ class HeldMessage(resource.Resource, CollectionMixin): msg = getUtility(IMessageStore).get_message_by_id(key) resource = dict( key=key, + # XXX convert _mod_{subject,hold_date,reason,sender,message_id} + # into top level values of the resource dict. data=data, msg=msg.as_string(), id=request_id, @@ -90,14 +93,15 @@ class HeldMessages(resource.Resource, CollectionMixin): def __init__(self, mlist): self._mlist = mlist + self._requests = None - def _resource_as_dict(self, req): + def _resource_as_dict(self, request): """See `CollectionMixin`.""" - key, data = self._requests.get_request(req.id) + key, data = self._requests.get_request(request.id) return dict( key=key, data=data, - id=req.id, + id=request.id, ) def _get_collection(self, request): @@ -108,9 +112,54 @@ class HeldMessages(resource.Resource, CollectionMixin): @resource.GET() def requests(self, request): """/lists/listname/held""" + # `request` is a restish.http.Request object. 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']) + + + +class SubscriptionRequests(resource.Resource, CollectionMixin): + """Resource for subscription and unsubscription requests.""" + + def __init__(self, mlist): + self._mlist = mlist + self._requests = None + + def _resource_as_dict(self, request_and_type): + """See `CollectionMixin`.""" + request, request_type = request_and_type + key, data = self._requests.get_request(request.id) + resource = dict( + key=key, + id=request.id, + ) + # Flatten the IRequest payload into the JSON representation. + resource.update(data) + # Add a key indicating what type of subscription request this is. + resource['type'] = request_type.name + return resource + + def _get_collection(self, request): + requests = IListRequests(self._mlist) + self._requests = requests + items = [] + for request_type in (RequestType.subscription, + RequestType.unsubscription): + for request in requests.of_type(request_type): + items.append((request, request_type)) + return items + + @resource.GET() + def requests(self, request): + """/lists/listname/requests""" + # `request` is a restish.http.Request object. + resource = self._make_collection(request) + return http.ok([], etag(resource)) + + @resource.child('{id}') + def subscription(self, request, segments, **kw): + pass -- 2.11.4.GIT