diff options
| author | Barry Warsaw | 2015-04-14 12:47:02 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2015-04-14 12:47:02 -0400 |
| commit | eab98485ec133dcc745618e4fd5b6054c902af05 (patch) | |
| tree | 8ed3883f88a496975bc4548c3a8a37764ab374c7 /src | |
| parent | 2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd (diff) | |
| parent | 85afb7bac938eb2c2f00507482886e1470bdcaa1 (diff) | |
| download | mailman-eab98485ec133dcc745618e4fd5b6054c902af05.tar.gz mailman-eab98485ec133dcc745618e4fd5b6054c902af05.tar.zst mailman-eab98485ec133dcc745618e4fd5b6054c902af05.zip | |
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/interfaces/member.py | 8 | ||||
| -rw-r--r-- | src/mailman/interfaces/roster.py | 21 | ||||
| -rw-r--r-- | src/mailman/model/docs/membership.rst | 38 | ||||
| -rw-r--r-- | src/mailman/model/member.py | 4 | ||||
| -rw-r--r-- | src/mailman/model/roster.py | 56 | ||||
| -rw-r--r-- | src/mailman/model/tests/test_roster.py | 52 |
6 files changed, 163 insertions, 16 deletions
diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py index 4e1ef6d37..d863e1ef1 100644 --- a/src/mailman/interfaces/member.py +++ b/src/mailman/interfaces/member.py @@ -175,6 +175,14 @@ class IMember(Interface): user = Attribute( """The user associated with this member.""") + subscriber = Attribute( + """The object representing how this member is subscribed. + + This will be an ``IAddress`` if the user is subscribed via an explicit + address, otherwise if the the user is subscribed via their preferred + address, it will be an ``IUser``. + """) + preferences = Attribute( """This member's preferences.""") diff --git a/src/mailman/interfaces/roster.py b/src/mailman/interfaces/roster.py index 5d0b9d6c2..af473a553 100644 --- a/src/mailman/interfaces/roster.py +++ b/src/mailman/interfaces/roster.py @@ -53,11 +53,26 @@ class IRoster(Interface): managed by this roster. """) - def get_member(address): + def get_member(email): """Get the member for the given address. - :param address: The email address to search for. - :type address: text + *Note* that it is possible for an email to be subscribed to a + mailing list twice, once through its explicit address and once + indirectly through a user's preferred address. In this case, + this API always returns the explicit address. Use + ``get_memberships()`` to return them all. + + :param email: The email address to search for. + :type email: string :return: The member if found, otherwise None :rtype: `IMember` or None """ + + def get_memberships(email): + """Get the memberships for the given address. + + :param email: The email address to search for. + :type email: string + :return: All the memberships associated with this email address. + :rtype: sequence of length 0, 1, or 2 of ``IMember`` + """ diff --git a/src/mailman/model/docs/membership.rst b/src/mailman/model/docs/membership.rst index 60ccd1ac1..0fd748d6a 100644 --- a/src/mailman/model/docs/membership.rst +++ b/src/mailman/model/docs/membership.rst @@ -228,6 +228,38 @@ regardless of their role. fperson@example.com MemberRole.nonmember +Subscriber type +=============== + +Members can be subscribed to a mailing list either via an explicit address, or +indirectly through a user's preferred address. Sometimes you want to know +which one it is. + +Herb subscribes to the mailing list via an explicit address. + + >>> herb = user_manager.create_address( + ... 'hperson@example.com', 'Herb Person') + >>> herb_member = mlist.subscribe(herb) + +Iris subscribes to the mailing list via her preferred address. + + >>> iris = user_manager.make_user( + ... 'iperson@example.com', 'Iris Person') + >>> preferred = list(iris.addresses)[0] + >>> from mailman.utilities.datetime import now + >>> preferred.verified_on = now() + >>> iris.preferred_address = preferred + >>> iris_member = mlist.subscribe(iris) + +When we need to know which way a member is subscribed, we can look at the this +attribute. + + >>> herb_member.subscriber + <Address: Herb Person <hperson@example.com> [not verified] at ...> + >>> iris_member.subscriber + <User "Iris Person" (5) at ...> + + Moderation actions ================== @@ -250,6 +282,8 @@ should go through the normal moderation checks. aperson@example.com MemberRole.member Action.defer bperson@example.com MemberRole.member Action.defer cperson@example.com MemberRole.member Action.defer + hperson@example.com MemberRole.member Action.defer + iperson@example.com MemberRole.member Action.defer Postings by nonmembers are held for moderator approval by default. @@ -272,7 +306,7 @@ though that the address they're changing to must be verified. >>> gwen_member = bee.subscribe(gwen_address) >>> for m in bee.members.members: ... print(m.member_id.int, m.mailing_list.list_id, m.address.email) - 7 bee.example.com gwen@example.com + 9 bee.example.com gwen@example.com Gwen gets a email address. @@ -288,7 +322,7 @@ Now her membership reflects the new address. >>> for m in bee.members.members: ... print(m.member_id.int, m.mailing_list.list_id, m.address.email) - 7 bee.example.com gperson@example.com + 9 bee.example.com gperson@example.com Events diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index ee6d246f5..e6e4933f9 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -135,6 +135,10 @@ class Member(Model): if self._address is None else getUtility(IUserManager).get_user(self._address.email)) + @property + def subscriber(self): + return (self._user if self._address is None else self._address) + def _lookup(self, preference, default=None): pref = getattr(self.preferences, preference) if pref is not None: diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py index e386ec3ad..da2ed4582 100644 --- a/src/mailman/model/roster.py +++ b/src/mailman/model/roster.py @@ -97,19 +97,48 @@ class AbstractRoster: yield member.address @dbconnection - def get_member(self, store, email): - """See `IRoster`.""" - results = self._query().filter( + def _get_all_memberships(self, store, email): + # Avoid circular imports. + from mailman.model.user import User + # Here's a query that finds all members subscribed with an explicit + # email address. + members_a = store.query(Member).filter( + Member.list_id == self._mlist.list_id, + Member.role == self.role, Address.email == email, Member.address_id == Address.id) - if results.count() == 0: + # Here's a query that finds all members subscribed with their + # preferred address. + members_u = store.query(Member).filter( + Member.list_id == self._mlist.list_id, + Member.role == self.role, + Address.email==email, + Member.user_id == User.id) + return members_a.union(members_u).all() + + def get_member(self, email): + """See ``IRoster``.""" + memberships = self._get_all_memberships(email) + count = len(memberships) + if count == 0: return None - elif results.count() == 1: - return results[0] - else: - raise AssertionError( - 'Too many matching member results: {0}'.format( - results.count())) + elif count == 1: + return memberships[0] + assert count == 2, 'Unexpected membership count: {}'.format(count) + # This is the case where the email address is subscribed both + # explicitly and indirectly through the preferred address. By + # definition, we return the explicit address membership only. + return (memberships[0] + if memberships[0]._address is not None + else memberships[1]) + + def get_memberships(self, email): + """See ``IRoster``.""" + memberships = self._get_all_memberships(email) + count = len(memberships) + assert 0 <= count <= 2, 'Unexpected membership count: {}'.format( + count) + return memberships @@ -298,3 +327,10 @@ class Memberships: raise AssertionError( 'Too many matching member results: {0}'.format( results.count())) + + @dbconnection + def get_memberships(self, store, address): + """See `IRoster`.""" + # 2015-04-14 BAW: See LP: #1444055 -- this currently exists just to + # pass a test. + raise NotImplementedError diff --git a/src/mailman/model/tests/test_roster.py b/src/mailman/model/tests/test_roster.py index 44735cf4b..ca950cfc5 100644 --- a/src/mailman/model/tests/test_roster.py +++ b/src/mailman/model/tests/test_roster.py @@ -26,7 +26,9 @@ __all__ = [ import unittest from mailman.app.lifecycle import create_list +from mailman.interfaces.address import IAddress from mailman.interfaces.member import DeliveryMode, MemberRole +from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager from mailman.testing.layers import ConfigLayer from mailman.utilities.datetime import now @@ -136,7 +138,8 @@ class TestMembershipsRoster(unittest.TestCase): self._ant = create_list('ant@example.com') self._bee = create_list('bee@example.com') user_manager = getUtility(IUserManager) - self._anne = user_manager.create_user('anne@example.com') + self._anne = user_manager.make_user( + 'anne@example.com', 'Anne Person') preferred = list(self._anne.addresses)[0] preferred.verified_on = now() self._anne.preferred_address = preferred @@ -144,9 +147,56 @@ class TestMembershipsRoster(unittest.TestCase): def test_no_memberships(self): # An unsubscribed user has no memberships. self.assertEqual(self._anne.memberships.member_count, 0) + self.assertIsNone(self._ant.members.get_member('anne@example.com')) + self.assertEqual( + self._ant.members.get_memberships('anne@example.com'), + []) def test_subscriptions(self): # Anne subscribes to a couple of mailing lists. self._ant.subscribe(self._anne) self._bee.subscribe(self._anne) self.assertEqual(self._anne.memberships.member_count, 2) + + def test_subscribed_as_user(self): + # Anne subscribes to a mailing list as a user and the member roster + # contains her membership. + self._ant.subscribe(self._anne) + self.assertEqual( + self._ant.members.get_member('anne@example.com').user, + self._anne) + memberships = self._ant.members.get_memberships('anne@example.com') + self.assertEqual( + [member.address.email for member in memberships], + ['anne@example.com']) + + def test_subscribed_as_user_and_address(self): + # Anne subscribes to a mailing list twice, once as a user and once + # with an explicit address. She has two memberships. + self._ant.subscribe(self._anne) + self._ant.subscribe(self._anne.preferred_address) + self.assertEqual(self._anne.memberships.member_count, 2) + self.assertEqual(self._ant.members.member_count, 2) + self.assertEqual( + [member.address.email for member in self._ant.members.members], + ['anne@example.com', 'anne@example.com']) + # get_member() is defined to return the explicit address. + member = self._ant.members.get_member('anne@example.com') + subscriber = member.subscriber + self.assertTrue(IAddress.providedBy(subscriber)) + self.assertFalse(IUser.providedBy(subscriber)) + # get_memberships() returns them all. + memberships = self._ant.members.get_memberships('anne@example.com') + self.assertEqual(len(memberships), 2) + as_address = (memberships[0] + if IAddress.providedBy(memberships[0].subscriber) + else memberships[1]) + as_user = (memberships[1] + if IUser.providedBy(memberships[1].subscriber) + else memberships[0]) + self.assertEqual(as_address.subscriber, self._anne.preferred_address) + self.assertEqual(as_user.subscriber, self._anne) + # All the email addresses match. + self.assertEqual( + [record.address.email for record in memberships], + ['anne@example.com', 'anne@example.com']) |
