summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2011-08-17 19:10:39 -0400
committerBarry Warsaw2011-08-17 19:10:39 -0400
commit6e7bfd50436c702aea5d392adcf2d63340ed3f69 (patch)
treedd315c07292da225a52caa87d0b00ec6f8558dc9 /src
parent441408fed20242e62d4e8f7b151ac8ec89c61ca4 (diff)
downloadmailman-6e7bfd50436c702aea5d392adcf2d63340ed3f69.tar.gz
mailman-6e7bfd50436c702aea5d392adcf2d63340ed3f69.tar.zst
mailman-6e7bfd50436c702aea5d392adcf2d63340ed3f69.zip
Basic infrastructure for fixing bug 827036.
* Use zope.events to signal when a mailing list has been created or deleted. * Register a handler for the ListDeletedEvent which cleans up member subscriptions. * Relax the criteria for find_members(), both internally and in the REST API, so that the subscriber is not required. E.g. you can now find all members of a mailing list.
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/docs/subscriptions.rst13
-rw-r--r--src/mailman/app/events.py38
-rw-r--r--src/mailman/app/subscriptions.py81
-rw-r--r--src/mailman/core/initialize.py2
-rw-r--r--src/mailman/interfaces/subscriptions.py2
-rw-r--r--src/mailman/model/tests/test_member.py33
-rw-r--r--src/mailman/rest/docs/membership.rst37
-rw-r--r--src/mailman/rest/members.py9
8 files changed, 176 insertions, 39 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..4beac8212
--- /dev/null
+++ b/src/mailman/app/events.py
@@ -0,0 +1,38 @@
+# 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.subscriptions import handle_ListDeleteEvent
+
+
+
+def initialize():
+ """Initialize global event subscribers."""
+ event.subscribers.extend([
+ handle_ListDeleteEvent,
+ ])
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index ab8c3d53c..57609a006 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_ListDeleteEvent(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/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/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/tests/test_member.py b/src/mailman/model/tests/test_member.py
index 7906d8983..0fc18cfa6 100644
--- a/src/mailman/model/tests/test_member.py
+++ b/src/mailman/model/tests/test_member.py
@@ -28,7 +28,9 @@ __all__ = [
import unittest
from mailman.app.lifecycle import create_list
+from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.member import MembershipError
+from mailman.interfaces.subscriptions import ISubscriptionService
from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
@@ -98,7 +100,38 @@ class TestMember(unittest.TestCase):
+class TestMembership(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_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestMember))
+ suite.addTest(unittest.makeSuite(TestMembership))
return suite
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