summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/docs/subscriptions.rst13
-rw-r--r--src/mailman/app/events.py39
-rw-r--r--src/mailman/app/moderator.py13
-rw-r--r--src/mailman/app/subscriptions.py81
-rw-r--r--src/mailman/chains/docs/moderation.rst (renamed from src/mailman/chains/docs/moderation.txt)0
-rw-r--r--src/mailman/core/initialize.py2
-rw-r--r--src/mailman/docs/NEWS.rst14
-rw-r--r--src/mailman/interfaces/listmanager.py32
-rw-r--r--src/mailman/interfaces/subscriptions.py2
-rw-r--r--src/mailman/model/docs/pending.rst (renamed from src/mailman/model/docs/pending.txt)0
-rw-r--r--src/mailman/model/listmanager.py10
-rw-r--r--src/mailman/model/tests/test_listmanager.py136
-rw-r--r--src/mailman/model/tests/test_user.py4
-rw-r--r--src/mailman/rest/docs/membership.rst37
-rw-r--r--src/mailman/rest/members.py9
-rw-r--r--src/mailman/testing/helpers.py1
16 files changed, 348 insertions, 45 deletions
diff --git a/src/mailman/app/docs/subscriptions.rst b/src/mailman/app/docs/subscriptions.rst
index 378dd0a40..8291132ce 100644
--- a/src/mailman/app/docs/subscriptions.rst
+++ b/src/mailman/app/docs/subscriptions.rst
@@ -79,7 +79,7 @@ If you know the member id for a specific member, you can get that member.
<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
If you know the member's address, you can find all their memberships, based on
-specific search criteria. At a minimum, you need the member's email address.
+specific search criteria.
::
>>> mlist2 = create_list('foo@example.com')
@@ -121,6 +121,17 @@ Memberships can also be searched for by user id.
<Member: anne <anne@example.com> on test@example.com
as MemberRole.moderator>]
+You can find all the memberships for a specific mailing list.
+
+ >>> service.find_members(fqdn_listname='test@example.com')
+ [<Member: anne <anne@example.com> on test@example.com
+ as MemberRole.member>,
+ <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>,
+ <Member: anne <anne@example.com> on test@example.com
+ as MemberRole.moderator>,
+ <Member: Bart Person <bart@example.com> on test@example.com
+ as MemberRole.member>]
+
You can find all the memberships for an address on a specific mailing list.
>>> service.find_members('anne@example.com', 'test@example.com')
diff --git a/src/mailman/app/events.py b/src/mailman/app/events.py
new file mode 100644
index 000000000..ac318f6b2
--- /dev/null
+++ b/src/mailman/app/events.py
@@ -0,0 +1,39 @@
+# Copyright (C) 2011 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/>.
+
+"""Global events."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ ]
+
+
+from zope import event
+
+from mailman.app import moderator, subscriptions
+
+
+
+def initialize():
+ """Initialize global event subscribers."""
+ event.subscribers.extend([
+ moderator.handle_ListDeletingEvent,
+ subscriptions.handle_ListDeletedEvent,
+ ])
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index 1b9f21d2a..0ba6f492a 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
+ 'handle_ListDeletingEvent',
'handle_message',
'handle_subscription',
'handle_unsubscription',
@@ -43,6 +44,7 @@ from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.action import Action
from mailman.interfaces.languages import ILanguageManager
+from mailman.interfaces.listmanager import ListDeletingEvent
from mailman.interfaces.member import (
AlreadySubscribedError, DeliveryMode, NotAMemberError)
from mailman.interfaces.messages import IMessageStore
@@ -355,3 +357,14 @@ def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
subject = _('Request to mailing list "$realname" rejected')
msg = UserNotification(recip, mlist.bounces_address, subject, text, lang)
msg.send(mlist)
+
+
+
+def handle_ListDeletingEvent(event):
+ if not isinstance(event, ListDeletingEvent):
+ 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)
+ for request in requestsdb.held_requests:
+ requestsdb.delete_request(request.id)
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index ab8c3d53c..f11494e75 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'SubscriptionService',
+ 'handle_ListDeleteEvent',
]
@@ -34,7 +35,8 @@ from mailman.app.membership import add_member, delete_member
from mailman.config import config
from mailman.core.constants import system_preferences
from mailman.interfaces.address import InvalidEmailAddressError
-from mailman.interfaces.listmanager import IListManager, NoSuchListError
+from mailman.interfaces.listmanager import (
+ IListManager, ListDeletedEvent, NoSuchListError)
from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.subscriptions import (
ISubscriptionService, MissingUserError)
@@ -93,43 +95,43 @@ class SubscriptionService:
assert members.count() == 1, 'Too many matching members'
return members[0]
- def find_members(self, subscriber, fqdn_listname=None, role=None):
+ def find_members(self, subscriber=None, fqdn_listname=None, role=None):
"""See `ISubscriptionService`."""
# If `subscriber` is a user id, then we'll search for all addresses
# which are controlled by the user, otherwise we'll just search for
# the given address.
user_manager = getUtility(IUserManager)
- query = None
- if isinstance(subscriber, basestring):
- # subscriber is an email address.
- address = user_manager.get_address(subscriber)
- user = user_manager.get_user(subscriber)
- # This probably could be made more efficient.
- if address is None or user is None:
- return []
- or_clause = Or(Member.address_id == address.id,
- Member.user_id == user.id)
- else:
- # subscriber is a user id.
- user = user_manager.get_user_by_id(unicode(subscriber))
- address_ids = list(address.id for address in user.addresses
- if address.id is not None)
- if len(address_ids) == 0 or user is None:
- return []
- or_clause = Or(Member.user_id == user.id,
- Member.address_id.is_in(address_ids))
- # The rest is the same, based on the given criteria.
- if fqdn_listname is None and role is None:
- query = or_clause
- elif fqdn_listname is None:
- query = And(Member.role == role, or_clause)
- elif role is None:
- query = And(Member.mailing_list == fqdn_listname, or_clause)
- else:
- query = And(Member.mailing_list == fqdn_listname,
- Member.role == role,
- or_clause)
- results = config.db.store.find(Member, query)
+ if subscriber is None and fqdn_listname is None and role is None:
+ return []
+ # Querying for the subscriber is the most complicated part, because
+ # the parameter can either be an email address or a user id.
+ query = []
+ if subscriber is not None:
+ if isinstance(subscriber, basestring):
+ # subscriber is an email address.
+ address = user_manager.get_address(subscriber)
+ user = user_manager.get_user(subscriber)
+ # This probably could be made more efficient.
+ if address is None or user is None:
+ return []
+ query.append(Or(Member.address_id == address.id,
+ Member.user_id == user.id))
+ else:
+ # subscriber is a user id.
+ user = user_manager.get_user_by_id(unicode(subscriber))
+ address_ids = list(address.id for address in user.addresses
+ if address.id is not None)
+ if len(address_ids) == 0 or user is None:
+ return []
+ query.append(Or(Member.user_id == user.id,
+ Member.address_id.is_in(address_ids)))
+ # Calculate the rest of the query expression, which will get And'd
+ # with the Or clause above (if there is one).
+ if fqdn_listname is not None:
+ query.append(Member.mailing_list == fqdn_listname)
+ if role is not None:
+ query.append(Member.role == role)
+ results = config.db.store.find(Member, And(*query))
return sorted(results, key=_membership_sort_key)
def __iter__(self):
@@ -177,3 +179,16 @@ class SubscriptionService:
raise NoSuchListError(fqdn_listname)
# XXX for now, no notification or user acknowledgement.
delete_member(mlist, address, False, False)
+
+
+
+def handle_ListDeletedEvent(event):
+ """Delete a mailing list's members when the list is deleted."""
+
+ if not isinstance(event, ListDeletedEvent):
+ return
+ # Find all the members still associated with the mailing list.
+ members = getUtility(ISubscriptionService).find_members(
+ fqdn_listname=event.fqdn_listname)
+ for member in members:
+ member.unsubscribe()
diff --git a/src/mailman/chains/docs/moderation.txt b/src/mailman/chains/docs/moderation.rst
index d80f9bd2c..d80f9bd2c 100644
--- a/src/mailman/chains/docs/moderation.txt
+++ b/src/mailman/chains/docs/moderation.rst
diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py
index 148d1a150..bf0f8b542 100644
--- a/src/mailman/core/initialize.py
+++ b/src/mailman/core/initialize.py
@@ -147,6 +147,7 @@ def initialize_2(debug=False, propagate_logs=None):
# Initialize the rules and chains. Do the imports here so as to avoid
# circular imports.
from mailman.app.commands import initialize as initialize_commands
+ from mailman.app.events import initialize as initialize_events
from mailman.core.chains import initialize as initialize_chains
from mailman.core.pipelines import initialize as initialize_pipelines
from mailman.core.rules import initialize as initialize_rules
@@ -155,6 +156,7 @@ def initialize_2(debug=False, propagate_logs=None):
initialize_chains()
initialize_pipelines()
initialize_commands()
+ initialize_events()
def initialize_3():
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 4a9e725bf..c2fa66905 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -19,6 +19,15 @@ Architecture
* mailman.qrunner log is renamed to mailman.runner
* master-qrunner.lck -> master.lck
* master-qrunner.pid -> master.pid
+ * Four new events are created, and notifications are sent during mailing list
+ lifecycle changes:
+ - ListCreatingEvent - sent before the mailing list is created
+ - ListCreatedEvent - sent after the mailing list is created
+ - ListDeletingEvent - sent before the mailing list is deleted
+ - ListDeletedEvent - sent after the mailing list is deleted
+ * Using the above events, when a mailing list is deleted, all its members are
+ deleted, as well as all held message requests (but not the held messages
+ themselves). (LP: 827036)
REST
----
@@ -26,9 +35,8 @@ REST
for consistency. This changes the REST API for mailing list
resources. (LP: #787599)
* New REST resource http://.../members/find can be POSTed to in order to find
- member records. Arguments are `subscriber` (email address to search for -
- required), `fqdn_listname` (optional), and `role` (i.e. MemberRole -
- optional). (LP: #799612)
+ member records. Optional arguments are `subscriber` (email address to
+ search for), `fqdn_listname`, and `role` (i.e. MemberRole). (LP: #799612)
* Fixed /lists/<fqdn_listname>/<role>/<email> (LP: #825570)
* Remove role plurals from /lists/<fqdn_listname/rosters/<role>
* Fixed incorrect error code for /members/<bogus> (LP: #821020). Given by
diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py
index 1efb4342f..c1b7721a9 100644
--- a/src/mailman/interfaces/listmanager.py
+++ b/src/mailman/interfaces/listmanager.py
@@ -23,6 +23,10 @@ __metaclass__ = type
__all__ = [
'IListManager',
'ListAlreadyExistsError',
+ 'ListCreatedEvent',
+ 'ListCreatingEvent',
+ 'ListDeletedEvent',
+ 'ListDeletingEvent',
'NoSuchListError',
]
@@ -51,6 +55,34 @@ class NoSuchListError(MailmanError):
return 'No such mailing list: {0.fqdn_listname}'.format(self)
+class ListCreatingEvent:
+ """A mailing list is about to be created."""
+
+ def __init__(self, fqdn_listname):
+ self.fqdn_listname = fqdn_listname
+
+
+class ListCreatedEvent:
+ """A mailing list was created."""
+
+ def __init__(self, mlist):
+ self.mailing_list = mlist
+
+
+class ListDeletingEvent:
+ """A mailing list is about to be deleted."""
+
+ def __init__(self, mailing_list):
+ self.mailing_list = mailing_list
+
+
+class ListDeletedEvent:
+ """A mailing list was deleted."""
+
+ def __init__(self, fqdn_listname):
+ self.fqdn_listname = fqdn_listname
+
+
class IListManager(Interface):
diff --git a/src/mailman/interfaces/subscriptions.py b/src/mailman/interfaces/subscriptions.py
index c6bc47d5d..aa2e318f0 100644
--- a/src/mailman/interfaces/subscriptions.py
+++ b/src/mailman/interfaces/subscriptions.py
@@ -68,7 +68,7 @@ class ISubscriptionService(Interface):
:rtype: `IMember`
"""
- def find_members(subscriber, fqdn_listname=None, role=None):
+ def find_members(subscriber=None, fqdn_listname=None, role=None):
"""Search for and return a specific member.
The members are sorted first by fully-qualified mailing list name,
diff --git a/src/mailman/model/docs/pending.txt b/src/mailman/model/docs/pending.rst
index 707e8a7fc..707e8a7fc 100644
--- a/src/mailman/model/docs/pending.txt
+++ b/src/mailman/model/docs/pending.rst
diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py
index a7f302883..16185e670 100644
--- a/src/mailman/model/listmanager.py
+++ b/src/mailman/model/listmanager.py
@@ -27,11 +27,14 @@ __all__ = [
import datetime
+from zope.event import notify
from zope.interface import implements
from mailman.config import config
from mailman.interfaces.address import InvalidEmailAddressError
-from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError
+from mailman.interfaces.listmanager import (
+ IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent,
+ ListDeletedEvent, ListDeletingEvent)
from mailman.model.mailinglist import MailingList
@@ -46,6 +49,7 @@ class ListManager:
listname, at, hostname = fqdn_listname.partition('@')
if len(hostname) == 0:
raise InvalidEmailAddressError(fqdn_listname)
+ notify(ListCreatingEvent(fqdn_listname))
mlist = config.db.store.find(
MailingList,
MailingList.list_name == listname,
@@ -55,6 +59,7 @@ class ListManager:
mlist = MailingList(fqdn_listname)
mlist.created_at = datetime.datetime.now()
config.db.store.add(mlist)
+ notify(ListCreatedEvent(mlist))
return mlist
def get(self, fqdn_listname):
@@ -70,7 +75,10 @@ class ListManager:
def delete(self, mlist):
"""See `IListManager`."""
+ fqdn_listname = mlist.fqdn_listname
+ notify(ListDeletingEvent(mlist))
config.db.store.remove(mlist)
+ notify(ListDeletedEvent(fqdn_listname))
@property
def mailing_lists(self):
diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py
new file mode 100644
index 000000000..8ea36d9b7
--- /dev/null
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -0,0 +1,136 @@
+# Copyright (C) 2011 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 ListManager."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'test_suite',
+ ]
+
+
+import unittest
+
+from zope.component import getUtility
+
+from mailman.app.lifecycle import create_list
+from mailman.app.moderator import hold_message
+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.subscriptions import ISubscriptionService
+from mailman.interfaces.usermanager import IUserManager
+from mailman.testing.helpers import (
+ event_subscribers, specialized_message_from_string)
+from mailman.testing.layers import ConfigLayer
+
+
+
+class TestListManager(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._events = []
+
+ def _record_event(self, event):
+ self._events.append(event)
+
+ def test_create_list_event(self):
+ # Test that creating a list in the list manager propagates the
+ # expected events.
+ with event_subscribers(self._record_event):
+ mlist = getUtility(IListManager).create('test@example.com')
+ self.assertEqual(len(self._events), 2)
+ self.assertTrue(isinstance(self._events[0], ListCreatingEvent))
+ self.assertEqual(self._events[0].fqdn_listname, 'test@example.com')
+ self.assertTrue(isinstance(self._events[1], ListCreatedEvent))
+ self.assertEqual(self._events[1].mailing_list, mlist)
+
+ def test_delete_list_event(self):
+ # Test that deleting a list in the list manager propagates the
+ # expected event.
+ mlist = create_list('another@example.com')
+ with event_subscribers(self._record_event):
+ getUtility(IListManager).delete(mlist)
+ self.assertEqual(len(self._events), 2)
+ self.assertTrue(isinstance(self._events[0], ListDeletingEvent))
+ self.assertEqual(self._events[0].mailing_list, mlist)
+ self.assertTrue(isinstance(self._events[1], ListDeletedEvent))
+ self.assertEqual(self._events[1].fqdn_listname, 'another@example.com')
+
+
+
+class TestListLifecycleEvents(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._ant = create_list('ant@example.com')
+ self._bee = create_list('bee@example.com')
+ self._usermanager = getUtility(IUserManager)
+
+ def test_members_are_deleted_when_mailing_list_is_deleted(self):
+ # When a mailing list with members is deleted, all the Member records
+ # are also deleted.
+ anne = self._usermanager.create_address('anne@example.com')
+ bart = self._usermanager.create_address('bart@example.com')
+ anne_ant = self._ant.subscribe(anne)
+ anne_bee = self._bee.subscribe(anne)
+ bart_ant = self._ant.subscribe(bart)
+ anne_ant_id = anne_ant.member_id
+ anne_bee_id = anne_bee.member_id
+ bart_ant_id = bart_ant.member_id
+ getUtility(IListManager).delete(self._ant)
+ service = getUtility(ISubscriptionService)
+ # We deleted the ant@example.com mailing list. Anne's and Bart's
+ # membership in this list should now be removed, but Anne's membership
+ # in bee@example.com should still exist.
+ self.assertEqual(service.get_member(anne_ant_id), None)
+ self.assertEqual(service.get_member(bart_ant_id), None)
+ self.assertEqual(service.get_member(anne_bee_id), anne_bee)
+
+ def test_requests_are_deleted_when_mailing_list_is_deleted(self):
+ # When a mailing list is deleted, its requests database is deleted
+ # too, e.g. all its message hold requests (but not the messages
+ # themselves).
+ msg = specialized_message_from_string("""\
+From: anne@example.com
+To: ant@example.com
+Subject: Hold me
+Message-ID: <argon>
+
+""")
+ request_id = hold_message(self._ant, msg)
+ 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)
+ request = requestsdb.get_request(request_id)
+ self.assertEqual(request, None)
+ saved_message = getUtility(IMessageStore).get_message_by_id('<argon>')
+ self.assertEqual(saved_message.as_string(), msg.as_string())
+
+
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestListManager))
+ suite.addTest(unittest.makeSuite(TestListLifecycleEvents))
+ return suite
diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py
index 072b2f7be..4112572bd 100644
--- a/src/mailman/model/tests/test_user.py
+++ b/src/mailman/model/tests/test_user.py
@@ -27,13 +27,13 @@ __all__ = [
import unittest
+from zope.component import getUtility
+
from mailman.app.lifecycle import create_list
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import now
-from zope.component import getUtility
-
class TestUser(unittest.TestCase):
diff --git a/src/mailman/rest/docs/membership.rst b/src/mailman/rest/docs/membership.rst
index 426b80acf..bed4f425d 100644
--- a/src/mailman/rest/docs/membership.rst
+++ b/src/mailman/rest/docs/membership.rst
@@ -308,6 +308,43 @@ example, we can search for all the memberships of a particular address.
start: 0
total_size: 2
+Or, we can find all the memberships for a particular mailing list.
+
+ >>> dump_json('http://localhost:9001/3.0/members/find', {
+ ... 'fqdn_listname': 'bee@example.com',
+ ... })
+ entry 0:
+ address: aperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/3
+ user: http://localhost:9001/3.0/users/3
+ entry 1:
+ address: bperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/1
+ user: http://localhost:9001/3.0/users/1
+ entry 2:
+ address: cperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: member
+ self_link: http://localhost:9001/3.0/members/2
+ user: http://localhost:9001/3.0/users/2
+ entry 3:
+ address: cperson@example.com
+ fqdn_listname: bee@example.com
+ http_etag: ...
+ role: owner
+ self_link: http://localhost:9001/3.0/members/7
+ user: http://localhost:9001/3.0/users/2
+ http_etag: "66836d0f23bed36fa9e0cda1e5dec7e5b0797743"
+ start: 0
+ total_size: 4
+
Or, we can find all the memberships for an address on a particular mailing
list.
diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py
index 0a4b99470..047c375bb 100644
--- a/src/mailman/rest/members.py
+++ b/src/mailman/rest/members.py
@@ -193,10 +193,11 @@ class FindMembers(_MemberBase):
def find(self, request):
"""Find a member"""
service = getUtility(ISubscriptionService)
- validator = Validator(fqdn_listname=unicode,
- subscriber=unicode,
- role=enum_validator(MemberRole),
- _optional=('fqdn_listname', 'role'))
+ validator = Validator(
+ fqdn_listname=unicode,
+ subscriber=unicode,
+ role=enum_validator(MemberRole),
+ _optional=('fqdn_listname', 'subscriber', 'role'))
members = service.find_members(**validator(request))
# We can't just return the _FoundMembers instance, because
# CollectionMixins have only a GET method, which is incompatible with
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 1a168dd0c..144ae1ffb 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -29,6 +29,7 @@ __all__ = [
'get_queue_messages',
'make_testable_runner',
'reset_the_world',
+ 'specialized_message_from_string',
'subscribe',
'wait_for_webservice',
]