diff options
| author | Barry Warsaw | 2011-08-17 19:10:39 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2011-08-17 19:10:39 -0400 |
| commit | 6e7bfd50436c702aea5d392adcf2d63340ed3f69 (patch) | |
| tree | dd315c07292da225a52caa87d0b00ec6f8558dc9 /src | |
| parent | 441408fed20242e62d4e8f7b151ac8ec89c61ca4 (diff) | |
| download | mailman-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.rst | 13 | ||||
| -rw-r--r-- | src/mailman/app/events.py | 38 | ||||
| -rw-r--r-- | src/mailman/app/subscriptions.py | 81 | ||||
| -rw-r--r-- | src/mailman/core/initialize.py | 2 | ||||
| -rw-r--r-- | src/mailman/interfaces/subscriptions.py | 2 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_member.py | 33 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.rst | 37 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 9 |
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 |
