summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2015-04-14 12:35:53 -0400
committerBarry Warsaw2015-04-14 12:35:53 -0400
commit85afb7bac938eb2c2f00507482886e1470bdcaa1 (patch)
treec65543aa61bc2dbdeaf4e914fb79160dcdffb864 /src
parent7317b94a0b746f0287ecbc5654ec544ce0112adb (diff)
downloadmailman-85afb7bac938eb2c2f00507482886e1470bdcaa1.tar.gz
mailman-85afb7bac938eb2c2f00507482886e1470bdcaa1.tar.zst
mailman-85afb7bac938eb2c2f00507482886e1470bdcaa1.zip
Added IMember.subscriber to definitively return how a member is subscribed to
the mailing list (via preferred address/user or explicit address). IMember.get_member() is defined to return the explicit address when members are subscribed in both ways. IMember.get_memberships() returns a sequence of length 0, 1, or 2 containing all the member records associated with the email address. Fixed the AbstractMemberRoster methods query to properly return subscriptions via the user's preferred address and via an explicit address.
Diffstat (limited to 'src')
-rw-r--r--src/mailman/interfaces/member.py8
-rw-r--r--src/mailman/interfaces/roster.py21
-rw-r--r--src/mailman/model/docs/membership.rst38
-rw-r--r--src/mailman/model/member.py4
-rw-r--r--src/mailman/model/roster.py56
-rw-r--r--src/mailman/model/tests/test_roster.py52
6 files changed, 162 insertions, 17 deletions
diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py
index c06cc95b1..10bb3bc59 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 91211c665..d319aa819 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -97,21 +97,48 @@ class AbstractRoster:
yield member.address
@dbconnection
- def get_member(self, store, address):
- """See `IRoster`."""
- results = store.query(Member).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 == address,
+ 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 +325,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'])