summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBarry Warsaw2015-04-14 12:47:02 -0400
committerBarry Warsaw2015-04-14 12:47:02 -0400
commiteab98485ec133dcc745618e4fd5b6054c902af05 (patch)
tree8ed3883f88a496975bc4548c3a8a37764ab374c7 /src
parent2787473f0bd4ca3efeadb7f44c8f61c3695e7ecd (diff)
parent85afb7bac938eb2c2f00507482886e1470bdcaa1 (diff)
downloadmailman-eab98485ec133dcc745618e4fd5b6054c902af05.tar.gz
mailman-eab98485ec133dcc745618e4fd5b6054c902af05.tar.zst
mailman-eab98485ec133dcc745618e4fd5b6054c902af05.zip
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, 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'])