summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.rst1
-rw-r--r--src/mailman/app/subscriptions.py72
-rw-r--r--src/mailman/app/tests/test_subscriptions.py31
-rw-r--r--src/mailman/interfaces/workflow.py14
-rw-r--r--src/mailman/model/tests/test_usermanager.py5
-rw-r--r--src/mailman/model/tests/test_workflow.py54
-rw-r--r--src/mailman/model/workflow.py12
7 files changed, 143 insertions, 46 deletions
diff --git a/TODO.rst b/TODO.rst
index 1adf03944..4a467ca96 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -1,4 +1,5 @@
* TO DO:
+ - add full RequestRecord to SubscriptionWorkflow ctor
- hook up sending of confirmation
- processing confirmations and continuing workflow
- get tokens for saving workflows
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index 2deec131b..22f4bdf56 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -25,6 +25,9 @@ __all__ = [
+import uuid
+
+from enum import Enum
from mailman.app.membership import add_member, delete_member
from mailman.app.moderator import hold_subscription
from mailman.app.workflow import Workflow
@@ -58,6 +61,11 @@ def _membership_sort_key(member):
return (member.list_id, member.address.email, member.role.value)
+class WhichSubscriber(Enum):
+ address = 1
+ user = 2
+
+
class SubscriptionWorkflow(Workflow):
"""Workflow of a subscription request."""
@@ -67,26 +75,63 @@ class SubscriptionWorkflow(Workflow):
'pre_approved',
'pre_confirmed',
'pre_verified',
+ 'address_key',
+ 'subscriber_key',
+ 'user_key',
)
- def __init__(self, mlist, subscriber, *,
+ def __init__(self, mlist, subscriber=None, *,
pre_verified=False, pre_confirmed=False, pre_approved=False):
super().__init__()
self.mlist = mlist
+ self.address = None
+ self.user = None
+ self.which = None
# The subscriber must be either an IUser or IAddress.
if IAddress.providedBy(subscriber):
self.address = subscriber
self.user = self.address.user
+ self.which = WhichSubscriber.address
elif IUser.providedBy(subscriber):
self.address = subscriber.preferred_address
self.user = subscriber
- else:
- raise AssertionError('subscriber is neither an IUser nor IAddress')
+ self.which = WhichSubscriber.user
self.subscriber = subscriber
self.pre_verified = pre_verified
self.pre_confirmed = pre_confirmed
self.pre_approved = pre_approved
+ @property
+ def user_key(self):
+ # For save.
+ return self.user.user_id.hex
+
+ @user_key.setter
+ def user_key(self, hex_key):
+ # For restore.
+ uid = uuid.UUID(hex_key)
+ self.user = getUtility(IUserManager).get_user_by_id(uid)
+ assert self.user is not None
+
+ @property
+ def address_key(self):
+ # For save.
+ return self.address.email
+
+ @address_key.setter
+ def address_key(self, email):
+ # For restore.
+ self.address = getUtility(IUserManager).get_address(email)
+ assert self.address is not None
+
+ @property
+ def subscriber_key(self):
+ return self.which.value
+
+ @subscriber_key.setter
+ def subscriber_key(self, key):
+ self.which = WhichSubscriber(key)
+
def _step_sanity_checks(self):
# Ensure that we have both an address and a user, even if the address
# is not verified. We can't set the preferred address until it is
@@ -160,8 +205,27 @@ class SubscriptionWorkflow(Workflow):
# subscription request in the database
request = RequestRecord(
self.address.email, self.subscriber.display_name,
+ # XXX Need to get these last to into the constructor.
DeliveryMode.regular, 'en')
- hold_subscription(self.mlist, request)
+ self.token = hold_subscription(self.mlist, request)
+ # Here's the next step in the workflow, assuming the moderator
+ # approves of the subscription. If they don't, the workflow and
+ # subscription request will just be thrown away.
+ self.push('subscribe_from_restored')
+ self.save()
+ # The workflow must stop running here.
+ raise StopIteration
+
+ def _step_subscribe_from_restored(self):
+ # Restore a little extra state that can't be stored in the database
+ # (because the order of setattr() on restore is indeterminate), then
+ # subscribe the user.
+ if self.which is WhichSubscriber.address:
+ self.subscriber = self.address
+ else:
+ assert self.which is WhichSubscriber.user
+ self.subscriber = self.user
+ self.push('do_subscription')
def _step_send_confirmation(self):
self._next.append('moderation_check')
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index 836e7f7b7..61d341d6d 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -85,8 +85,8 @@ class TestSubscriptionWorkflow(unittest.TestCase):
def test_user_or_address_required(self):
# The `subscriber` attribute must be a user or address.
- self.assertRaises(
- AssertionError, SubscriptionWorkflow, self._mlist, 'not a user')
+ workflow = SubscriptionWorkflow(self._mlist)
+ self.assertRaises(AssertionError, list, workflow)
def test_sanity_checks_address(self):
# Ensure that the sanity check phase, when given an IAddress, ends up
@@ -325,8 +325,34 @@ class TestSubscriptionWorkflow(unittest.TestCase):
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address, anne)
+ def test_moderator_approves(self):
+ # The workflow runs until moderator approval is required, at which
+ # point the workflow is saved. Once the moderator approves, the
+ # workflow resumes and the user is subscribed.
+ self._mlist.subscription_policy = SubscriptionPolicy.moderate
+ anne = self._user_manager.create_address(self._anne)
+ workflow = SubscriptionWorkflow(self._mlist, anne,
+ pre_verified=True,
+ pre_confirmed=True)
+ # Consume the entire state machine.
+ list(workflow)
+ # The user is not currently subscribed to the mailing list.
+ member = self._mlist.regular_members.get_member(self._anne)
+ self.assertIsNone(member)
+ # Create a new workflow with the previous workflow's save token, and
+ # restore its state. This models an approved subscription and should
+ # result in the user getting subscribed.
+ approved_workflow = SubscriptionWorkflow(self._mlist)
+ approved_workflow.token = workflow.token
+ approved_workflow.restore()
+ list(approved_workflow)
+ # Now the user is subscribed to the mailing list.
+ member = self._mlist.regular_members.get_member(self._anne)
+ self.assertEqual(member.address, anne)
+
# XXX
+ @unittest.expectedFailure
def test_preverified_address_joins_open_list(self):
# The mailing list has an open subscription policy, so the subscriber
# becomes a member with no human intervention.
@@ -346,6 +372,7 @@ class TestSubscriptionWorkflow(unittest.TestCase):
self.assertIsNotNone(anne.user)
self.assertIsNotNone(self._mlist.subscribers.get_member(self._anne))
+ @unittest.expectedFailure
def test_verified_address_joins_moderated_list(self):
# The mailing list is moderated but the subscriber is not a verified
# address and the subscription request is not pre-verified.
diff --git a/src/mailman/interfaces/workflow.py b/src/mailman/interfaces/workflow.py
index 49fbc85d4..4a4fb4412 100644
--- a/src/mailman/interfaces/workflow.py
+++ b/src/mailman/interfaces/workflow.py
@@ -32,7 +32,7 @@ class IWorkflowState(Interface):
name = Attribute('The name of the workflow.')
- key = Attribute('A unique key identifying the workflow instance.')
+ token = Attribute('A unique key identifying the workflow instance.')
step = Attribute("This workflow's next step.")
@@ -43,24 +43,24 @@ class IWorkflowState(Interface):
class IWorkflowStateManager(Interface):
"""The workflow states manager."""
- def save(name, key, step, data=None):
+ def save(name, token, step, data=None):
"""Save the state of a workflow.
:param name: The name of the workflow.
:type name: str
- :param key: A unique key identifying this workflow instance.
- :type key: str
+ :param token: A unique token identifying this workflow instance.
+ :type token: str
:param step: The next step for this workflow.
:type step: str
:param data: Additional data (workflow-specific).
:type data: str
"""
- def restore(name, key):
+ def restore(name, token):
"""Get the saved state for a workflow or None if nothing was saved.
:param name: The name of the workflow.
:type name: str
- :param key: A unique key identifying this workflow instance.
- :type key: str
+ :param token: A unique token identifying this workflow instance.
+ :type token: str
"""
diff --git a/src/mailman/model/tests/test_usermanager.py b/src/mailman/model/tests/test_usermanager.py
index 31f1a7275..e441ed713 100644
--- a/src/mailman/model/tests/test_usermanager.py
+++ b/src/mailman/model/tests/test_usermanager.py
@@ -80,3 +80,8 @@ class TestUserManager(unittest.TestCase):
user = self._usermanager.create_user('anne@example.com', 'Anne Person')
other_user = self._usermanager.make_user('anne@example.com')
self.assertIs(user, other_user)
+
+ def test_get_user_by_id(self):
+ original = self._usermanager.make_user('anne@example.com')
+ copy = self._usermanager.get_user_by_id(original.user_id)
+ self.assertEqual(original, copy)
diff --git a/src/mailman/model/tests/test_workflow.py b/src/mailman/model/tests/test_workflow.py
index b5e915df4..6081e5b57 100644
--- a/src/mailman/model/tests/test_workflow.py
+++ b/src/mailman/model/tests/test_workflow.py
@@ -39,72 +39,72 @@ class TestWorkflow(unittest.TestCase):
def test_save_restore_workflow(self):
# Save and restore a workflow.
name = 'ant'
- key = 'bee'
+ token = 'bee'
step = 'cat'
data = 'dog'
- self._manager.save(name, key, step, data)
- workflow = self._manager.restore(name, key)
+ self._manager.save(name, token, step, data)
+ workflow = self._manager.restore(name, token)
self.assertEqual(workflow.name, name)
- self.assertEqual(workflow.key, key)
+ self.assertEqual(workflow.token, token)
self.assertEqual(workflow.step, step)
self.assertEqual(workflow.data, data)
def test_save_restore_workflow_without_step(self):
# Save and restore a workflow that contains no step.
name = 'ant'
- key = 'bee'
+ token = 'bee'
data = 'dog'
- self._manager.save(name, key, data=data)
- workflow = self._manager.restore(name, key)
+ self._manager.save(name, token, data=data)
+ workflow = self._manager.restore(name, token)
self.assertEqual(workflow.name, name)
- self.assertEqual(workflow.key, key)
+ self.assertEqual(workflow.token, token)
self.assertIsNone(workflow.step)
self.assertEqual(workflow.data, data)
def test_save_restore_workflow_without_data(self):
# Save and restore a workflow that contains no data.
name = 'ant'
- key = 'bee'
+ token = 'bee'
step = 'cat'
- self._manager.save(name, key, step)
- workflow = self._manager.restore(name, key)
+ self._manager.save(name, token, step)
+ workflow = self._manager.restore(name, token)
self.assertEqual(workflow.name, name)
- self.assertEqual(workflow.key, key)
+ self.assertEqual(workflow.token, token)
self.assertEqual(workflow.step, step)
self.assertIsNone(workflow.data)
def test_save_restore_workflow_without_step_or_data(self):
# Save and restore a workflow that contains no step or data.
name = 'ant'
- key = 'bee'
- self._manager.save(name, key)
- workflow = self._manager.restore(name, key)
+ token = 'bee'
+ self._manager.save(name, token)
+ workflow = self._manager.restore(name, token)
self.assertEqual(workflow.name, name)
- self.assertEqual(workflow.key, key)
+ self.assertEqual(workflow.token, token)
self.assertIsNone(workflow.step)
self.assertIsNone(workflow.data)
def test_restore_workflow_with_no_matching_name(self):
# Try to restore a workflow that has no matching name in the database.
name = 'ant'
- key = 'bee'
- self._manager.save(name, key)
- workflow = self._manager.restore('ewe', key)
+ token = 'bee'
+ self._manager.save(name, token)
+ workflow = self._manager.restore('ewe', token)
self.assertIsNone(workflow)
- def test_restore_workflow_with_no_matching_key(self):
- # Try to restore a workflow that has no matching key in the database.
+ def test_restore_workflow_with_no_matching_token(self):
+ # Try to restore a workflow that has no matching token in the database.
name = 'ant'
- key = 'bee'
- self._manager.save(name, key)
+ token = 'bee'
+ self._manager.save(name, token)
workflow = self._manager.restore(name, 'fly')
self.assertIsNone(workflow)
- def test_restore_workflow_with_no_matching_key_or_name(self):
- # Try to restore a workflow that has no matching key or name in the
+ def test_restore_workflow_with_no_matching_token_or_name(self):
+ # Try to restore a workflow that has no matching token or name in the
# database.
name = 'ant'
- key = 'bee'
- self._manager.save(name, key)
+ token = 'bee'
+ self._manager.save(name, token)
workflow = self._manager.restore('ewe', 'fly')
self.assertIsNone(workflow)
diff --git a/src/mailman/model/workflow.py b/src/mailman/model/workflow.py
index 53c9f05ea..d9f23f53b 100644
--- a/src/mailman/model/workflow.py
+++ b/src/mailman/model/workflow.py
@@ -38,7 +38,7 @@ class WorkflowState(Model):
__tablename__ = 'workflowstate'
name = Column(Unicode, primary_key=True)
- key = Column(Unicode, primary_key=True)
+ token = Column(Unicode, primary_key=True)
step = Column(Unicode)
data = Column(Unicode)
@@ -49,17 +49,17 @@ class WorkflowStateManager:
"""See `IWorkflowStateManager`."""
@dbconnection
- def save(self, store, name, key, step=None, data=None):
+ def save(self, store, name, token, step=None, data=None):
"""See `IWorkflowStateManager`."""
- state = store.query(WorkflowState).get((name, key))
+ state = store.query(WorkflowState).get((name, token))
if state is None:
- state = WorkflowState(name=name, key=key, step=step, data=data)
+ state = WorkflowState(name=name, token=token, step=step, data=data)
store.add(state)
else:
state.step = step
state.data = data
@dbconnection
- def restore(self, store, name, key):
+ def restore(self, store, name, token):
"""See `IWorkflowStateManager`."""
- return store.query(WorkflowState).get((name, key))
+ return store.query(WorkflowState).get((name, token))