summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/subscriptions.py177
-rw-r--r--src/mailman/app/tests/test_subscriptions.py124
-rw-r--r--src/mailman/config/configure.zcml5
-rw-r--r--src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py41
-rw-r--r--src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py28
-rw-r--r--src/mailman/interfaces/mailinglist.py16
-rw-r--r--src/mailman/interfaces/workflowstate.py70
-rw-r--r--src/mailman/model/mailinglist.py3
-rw-r--r--src/mailman/model/roster.py4
-rw-r--r--src/mailman/model/workflowstate.py66
-rw-r--r--src/mailman/rest/root.py1
-rw-r--r--src/mailman/runners/docs/outgoing.rst2
-rw-r--r--src/mailman/styles/base.py4
-rw-r--r--src/mailman/utilities/importer.py3
-rw-r--r--src/mailman/utilities/tests/test_import.py27
15 files changed, 560 insertions, 11 deletions
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index cc363b30d..1e45da43e 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -19,10 +19,13 @@
__all__ = [
'SubscriptionService',
+ 'SubscriptionWorkflow',
'handle_ListDeletingEvent',
]
+import json
+from collections import deque
from operator import attrgetter
from sqlalchemy import and_, or_
from uuid import UUID
@@ -30,18 +33,23 @@ from zope.component import getUtility
from zope.interface import implementer
from mailman.app.membership import add_member, delete_member
+from mailman.app.moderator import hold_subscription
from mailman.core.constants import system_preferences
from mailman.database.transaction import dbconnection
+from mailman.interfaces.address import IAddress
from mailman.interfaces.listmanager import (
IListManager, ListDeletingEvent, NoSuchListError)
+from mailman.interfaces.mailinglist import SubscriptionPolicy
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.subscriptions import (
ISubscriptionService, MissingUserError, RequestRecord)
+from mailman.interfaces.user import IUser
from mailman.interfaces.usermanager import IUserManager
+from mailman.interfaces.workflowstate import IWorkflowStateManager
from mailman.model.member import Member
+from mailman.utilities.datetime import now
-
def _membership_sort_key(member):
"""Sort function for find_members().
@@ -51,7 +59,171 @@ def _membership_sort_key(member):
return (member.list_id, member.address.email, member.role.value)
-
+class Workflow:
+ """Generic workflow."""
+ # TODO: move this class to a more generic module
+
+ _save_key = ""
+ _save_attributes = []
+ _initial_state = []
+
+ def __init__(self):
+ self._next = deque(self._initial_state)
+
+ def __iter__(self):
+ return self
+
+ def _pop(self):
+ name = self._next.popleft()
+ step = getattr(self, '_step_{}'.format(name))
+ return step, name
+
+ def __next__(self):
+ try:
+ step, name = self._pop()
+ step()
+ except IndexError:
+ raise StopIteration
+ except:
+ raise
+
+ def save_state(self):
+ state_manager = getUtility(IWorkflowStateManager)
+ data = {attr: getattr(self, attr) for attr in self._save_attributes}
+ # Note: only the next step is saved, not the whole stack. Not an issue
+ # since there's never more than a single step in the queue anyway.
+ # If we want to support more than a single step in the queue AND want
+ # to support state saving/restoring, change this method and the
+ # restore_state() method.
+ if len(self._next) == 0:
+ step = None
+ elif len(self._next) == 1:
+ step = self._next[0]
+ else:
+ raise AssertionError(
+ "Can't save a workflow state with more than one step "
+ "in the queue")
+ state_manager.save(
+ self.__class__.__name__,
+ self._save_key,
+ step,
+ json.dumps(data))
+
+ def restore_state(self):
+ state_manager = getUtility(IWorkflowStateManager)
+ state = state_manager.restore(self.__class__.__name__, self._save_key)
+ if state is not None:
+ self._next.clear()
+ if state.step:
+ self._next.append(state.step)
+ if state.data is not None:
+ for attr, value in json.loads(state.data).items():
+ setattr(self, attr, value)
+
+
+class SubscriptionWorkflow(Workflow):
+ """Workflow of a subscription request."""
+
+ _save_attributes = ["pre_verified", "pre_confirmed", "pre_approved"]
+ _initial_state = ["verification_check"]
+
+ def __init__(self, mlist, subscriber,
+ pre_verified, pre_confirmed, pre_approved):
+ super(SubscriptionWorkflow, self).__init__()
+ self.mlist = mlist
+ # The subscriber must be either an IUser or IAddress.
+ if IAddress.providedBy(subscriber):
+ self.address = subscriber
+ self.user = self.address.user
+ elif IUser.providedBy(subscriber):
+ self.address = subscriber.preferred_address
+ self.user = subscriber
+ self.subscriber = subscriber
+ self.pre_verified = pre_verified
+ self.pre_confirmed = pre_confirmed
+ self.pre_approved = pre_approved
+ # State saving
+ self._save_key = "{}:{}".format(self.mlist.list_id, self.address.email)
+
+ def _maybe_set_preferred_address(self):
+ if self.user is None:
+ # The address has no linked user so create one, link it, and set
+ # the user's preferred address.
+ assert self.address is not None, 'No address or user'
+ self.user = getUtility(IUserManager).make_user(self.address.email)
+ self.user.preferred_address = self.address
+ elif self.user.preferred_address is None:
+ assert self.address is not None, 'No address or user'
+ # The address has a linked user, but no preferred address is set
+ # yet. This is required, so use the address.
+ self.user.preferred_address = self.address
+
+ def _step_verification_check(self):
+ if self.address.verified_on is not None:
+ # The address is already verified. Give the user a preferred
+ # address if it doesn't already have one. We may still have to do
+ # a subscription confirmation check. See below.
+ self._maybe_set_preferred_address()
+ else:
+ # The address is not yet verified. Maybe we're pre-verifying it.
+ # If so, we also want to give the user a preferred address if it
+ # doesn't already have one. We may still have to do a
+ # subscription confirmation check. See below.
+ if self.pre_verified:
+ self.address.verified_on = now()
+ self._maybe_set_preferred_address()
+ else:
+ # Since the address was not already verified, and not
+ # pre-verified, we have to send a confirmation check, which
+ # doubles as a verification step. Skip to that now.
+ self._next.append("send_confirmation")
+ return
+ self._next.append("confirmation_check")
+
+ def _step_confirmation_check(self):
+ # Must the user confirm their subscription request? If the policy is
+ # open subscriptions, then we need neither confirmation nor moderator
+ # approval, so just subscribe them now.
+ if self.mlist.subscription_policy == SubscriptionPolicy.open:
+ self._next.append("do_subscription")
+ elif self.pre_confirmed:
+ # No confirmation is necessary. We can skip to seeing whether a
+ # moderator confirmation is necessary.
+ self._next.append("moderation_check")
+ else:
+ self._next.append("send_confirmation")
+
+ def _step_send_confirmation(self):
+ self._next.append("moderation_check")
+ self.save_state()
+ self._next.clear() # stop iteration until we get confirmation
+ # XXX: create the Pendable, send the ConfirmationNeededEvent
+ # (see Registrar.register)
+
+ def _step_moderation_check(self):
+ # Does the moderator need to approve the subscription request?
+ if not self.pre_approved and self.mlist.subscription_policy in (
+ SubscriptionPolicy.moderate,
+ SubscriptionPolicy.confirm_then_moderate):
+ self._next.append("get_moderator_approval")
+ else:
+ # The moderator does not need to approve the subscription, so go
+ # ahead and do that now.
+ self._next.append("do_subscription")
+
+ def _step_get_moderator_approval(self):
+ # In order to get the moderator's approval, we need to hold the
+ # subscription request in the database
+ request = RequestRecord(
+ self.address.email, self.subscriber.display_name,
+ DeliveryMode.regular, 'en')
+ hold_subscription(self.mlist, request)
+
+ def _step_do_subscription(self):
+ # We can immediately subscribe the user to the mailing list.
+ self.mlist.subscribe(self.subscriber)
+
+
@implementer(ISubscriptionService)
class SubscriptionService:
"""Subscription services for the REST API."""
@@ -168,7 +340,6 @@ class SubscriptionService:
delete_member(mlist, email, False, False)
-
def handle_ListDeletingEvent(event):
"""Delete a mailing list's members when the list is being deleted."""
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index 8ba5f52ff..b19dc5398 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -18,7 +18,8 @@
"""Tests for the subscription service."""
__all__ = [
- 'TestJoin'
+ 'TestJoin',
+ 'TestSubscriptionWorkflow',
]
@@ -26,11 +27,17 @@ import uuid
import unittest
from mailman.app.lifecycle import create_list
+from mailman.app.subscriptions import Workflow, SubscriptionWorkflow
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.member import MemberRole, MissingPreferredAddressError
+from mailman.interfaces.requests import IListRequests, RequestType
from mailman.interfaces.subscriptions import (
MissingUserError, ISubscriptionService)
from mailman.testing.layers import ConfigLayer
+from mailman.interfaces.mailinglist import SubscriptionPolicy
+from mailman.interfaces.usermanager import IUserManager
+from mailman.utilities.datetime import now
+from mock import Mock
from zope.component import getUtility
@@ -65,3 +72,118 @@ class TestJoin(unittest.TestCase):
self._service.join,
'test.example.com', anne.user.user_id,
role=MemberRole.owner)
+
+
+
+class TestWorkflow(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self.workflow = Workflow()
+ self.workflow._test_attribute = "test-value"
+ self.workflow._step_test = Mock()
+ self.workflow._next.append("test")
+
+ def test_iter_steps(self):
+ next(self.workflow)
+ self.assertTrue(self.workflow._step_test.called)
+ self.assertEqual(len(self.workflow._next), 0)
+ try:
+ next(self.workflow)
+ except StopIteration:
+ pass
+ else:
+ self.fail()
+
+ def test_save_restore(self):
+ self.workflow.save_state()
+ # Now create a new instance and restore
+ new_workflow = Workflow()
+ self.assertEqual(len(new_workflow._next), 0)
+ self.assertFalse(hasattr(new_workflow, "_test_attribute"))
+ new_workflow.restore_state()
+ self.assertEqual(len(new_workflow._next), 1)
+ self.assertEqual(new_workflow._next[0], "test")
+ self.assertEqual(self.workflow._test_attribute, "test-value")
+
+ def test_save_restore_no_next_step(self):
+ self.workflow._next.clear()
+ self.workflow.save_state()
+ # Now create a new instance and restore
+ new_workflow = Workflow()
+ new_workflow._next.append("test")
+ new_workflow.restore_state()
+ self.assertEqual(len(new_workflow._next), 0)
+
+
+
+class TestSubscriptionWorkflow(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('test@example.com')
+ self._anne = 'anne@example.com'
+ self._user_manager = getUtility(IUserManager)
+
+ 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.
+ self._mlist.subscription_policy = SubscriptionPolicy.open
+ anne = self._user_manager.create_address(self._anne, 'Anne Person')
+ self.assertIsNone(anne.verified_on)
+ self.assertIsNone(anne.user)
+ self.assertIsNone(self._mlist.subscribers.get_member(self._anne))
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=True, pre_confirmed=False, pre_approved=False)
+ # Run the state machine to the end. The result is that her address
+ # will be verified, linked to a user, and subscribed to the mailing
+ # list.
+ list(workflow)
+ self.assertIsNotNone(anne.verified_on)
+ self.assertIsNotNone(anne.user)
+ self.assertIsNotNone(self._mlist.subscribers.get_member(self._anne))
+
+ 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.
+ # A confirmation email must be sent, it will serve as the verification
+ # email too.
+ anne = self._user_manager.create_address(self._anne, 'Anne Person')
+ request_db = IListRequests(self._mlist)
+ def _do_check():
+ anne.verified_on = now()
+ self.assertIsNone(self._mlist.subscribers.get_member(self._anne))
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=False, pre_confirmed=True, pre_approved=False)
+ # Run the state machine to the end.
+ list(workflow)
+ # Look in the requests db
+ requests = list(request_db.of_type(RequestType.subscription))
+ self.assertEqual(len(requests), 1)
+ self.assertEqual(requests[0].key, anne.email)
+ request_db.delete_request(requests[0].id)
+ self._mlist.subscription_policy = SubscriptionPolicy.moderate
+ _do_check()
+ self._mlist.subscription_policy = \
+ SubscriptionPolicy.confirm_then_moderate
+ _do_check()
+
+ def test_confirmation_required(self):
+ # Tests subscriptions where user confirmation is required
+ self._mlist.subscription_policy = \
+ SubscriptionPolicy.confirm_then_moderate
+ anne = self._user_manager.create_address(self._anne, 'Anne Person')
+ self.assertIsNone(self._mlist.subscribers.get_member(self._anne))
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=True, pre_confirmed=False, pre_approved=True)
+ # Run the state machine to the end.
+ list(workflow)
+ # A confirmation request must be pending
+ # TODO: test it
+ # Now restore and re-run the state machine as if we got the confirmation
+ workflow.restore_state()
+ list(workflow)
+ self.assertIsNotNone(self._mlist.subscribers.get_member(self._anne))
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index 24061f0f0..f6cb6dac1 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -117,4 +117,9 @@
factory="mailman.app.templates.TemplateLoader"
/>
+ <utility
+ provides="mailman.interfaces.workflowstate.IWorkflowStateManager"
+ factory="mailman.model.workflowstate.WorkflowStateManager"
+ />
+
</configure>
diff --git a/src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py b/src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py
new file mode 100644
index 000000000..d5f8c3d96
--- /dev/null
+++ b/src/mailman/database/alembic/versions/16c2b25c7b_list_subscription_policy.py
@@ -0,0 +1,41 @@
+"""List subscription policy
+
+Revision ID: 16c2b25c7b
+Revises: 33e1f5f6fa8
+Create Date: 2015-03-21 11:00:44.634883
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '16c2b25c7b'
+down_revision = '33e1f5f6fa8'
+
+from alembic import op
+import sqlalchemy as sa
+
+from mailman.database.types import Enum
+from mailman.interfaces.mailinglist import SubscriptionPolicy
+
+
+def upgrade():
+
+ ### Update the schema
+ op.add_column('mailinglist', sa.Column(
+ 'subscription_policy', Enum(SubscriptionPolicy), nullable=True))
+
+ ### Now migrate the data
+ # don't import the table definition from the models, it may break this
+ # migration when the model is updated in the future (see the Alembic doc)
+ mlist = sa.sql.table('mailinglist',
+ sa.sql.column('subscription_policy', Enum(SubscriptionPolicy))
+ )
+ # there were no enforced subscription policy before, so all lists are
+ # considered open
+ op.execute(mlist.update().values(
+ {'subscription_policy': op.inline_literal(SubscriptionPolicy.open)}))
+
+
+def downgrade():
+ if op.get_bind().dialect.name != 'sqlite':
+ # SQLite does not support dropping columns.
+ op.drop_column('mailinglist', 'subscription_policy')
diff --git a/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py b/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py
new file mode 100644
index 000000000..e6608b6ce
--- /dev/null
+++ b/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py
@@ -0,0 +1,28 @@
+"""Workflow state table
+
+Revision ID: 2bb9b382198
+Revises: 16c2b25c7b
+Create Date: 2015-03-25 18:09:18.338790
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2bb9b382198'
+down_revision = '16c2b25c7b'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.create_table('workflowstate',
+ sa.Column('name', sa.Unicode(), nullable=False),
+ sa.Column('key', sa.Unicode(), nullable=False),
+ sa.Column('step', sa.Unicode(), nullable=True),
+ sa.Column('data', sa.Unicode(), nullable=True),
+ sa.PrimaryKeyConstraint('name', 'key')
+ )
+
+
+def downgrade():
+ op.drop_table('workflowstate')
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index 23d2fadf4..f112b2a11 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -25,6 +25,7 @@ __all__ = [
'IMailingList',
'Personalization',
'ReplyToMunging',
+ 'SubscriptionPolicy',
]
@@ -53,6 +54,18 @@ class ReplyToMunging(Enum):
explicit_header = 2
+class SubscriptionPolicy(Enum):
+ # Neither confirmation, nor moderator approval is required.
+ open = 0
+ # The user must confirm the subscription.
+ confirm = 1
+ # The moderator must approve the subscription.
+ moderate = 2
+ # The user must first confirm their subscription, and then if that is
+ # successful, the moderator must also approve it.
+ confirm_then_moderate = 3
+
+
class IMailingList(Interface):
"""A mailing list."""
@@ -234,6 +247,9 @@ class IMailingList(Interface):
deliver disabled or not, or of the type of digest they are to
receive.""")
+ subscription_policy = Attribute(
+ """The policy for subscribing new members to the list.""")
+
subscribers = Attribute(
"""An iterator over all IMembers subscribed to this list, with any
role.
diff --git a/src/mailman/interfaces/workflowstate.py b/src/mailman/interfaces/workflowstate.py
new file mode 100644
index 000000000..186386170
--- /dev/null
+++ b/src/mailman/interfaces/workflowstate.py
@@ -0,0 +1,70 @@
+# Copyright (C) 2007-2015 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/>.
+
+"""Interface describing the state of a workflow."""
+
+__all__ = [
+ 'IWorkflowState',
+ 'IWorkflowStateManager',
+ ]
+
+
+from zope.interface import Interface, Attribute
+
+
+
+class IWorkflowState(Interface):
+ """The state of a workflow."""
+
+ name = Attribute(
+ """The name of the workflow.""")
+
+ key = Attribute(
+ """A unique key identifying the workflow instance.""")
+
+ step = Attribute(
+ """This workflow's next step.""")
+
+ data = Attribute(
+ """Additional data (may be JSON-encodedeJSON .""")
+
+
+
+class IWorkflowStateManager(Interface):
+ """The workflow states manager."""
+
+ def save(name, key, 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 step: The next step for this workflow.
+ :type step: str
+ :param data: Additional data (workflow-specific).
+ :type data: str
+ """
+
+ def restore(name, key):
+ """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
+ """
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index cef272437..f04c534e1 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -38,7 +38,7 @@ from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.mailinglist import (
IAcceptableAlias, IAcceptableAliasSet, IListArchiver, IListArchiverSet,
- IMailingList, Personalization, ReplyToMunging)
+ IMailingList, Personalization, ReplyToMunging, SubscriptionPolicy)
from mailman.interfaces.member import (
AlreadySubscribedError, MemberRole, MissingPreferredAddressError,
SubscriptionEvent)
@@ -183,6 +183,7 @@ class MailingList(Model):
send_goodbye_message = Column(Boolean)
send_welcome_message = Column(Boolean)
subject_prefix = Column(Unicode)
+ subscription_policy = Column(Enum(SubscriptionPolicy))
topics = Column(PickleType)
topics_bodylines_limit = Column(Integer)
topics_enabled = Column(Boolean)
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 91211c665..ef24d896b 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -99,9 +99,7 @@ class AbstractRoster:
@dbconnection
def get_member(self, store, address):
"""See `IRoster`."""
- results = store.query(Member).filter(
- Member.list_id == self._mlist.list_id,
- Member.role == self.role,
+ results = self._query().filter(
Address.email == address,
Member.address_id == Address.id)
if results.count() == 0:
diff --git a/src/mailman/model/workflowstate.py b/src/mailman/model/workflowstate.py
new file mode 100644
index 000000000..229a2240b
--- /dev/null
+++ b/src/mailman/model/workflowstate.py
@@ -0,0 +1,66 @@
+# Copyright (C) 2007-2015 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/>.
+
+"""Model for workflow states."""
+
+__all__ = [
+ 'WorkflowState',
+ 'WorkflowStateManager',
+ ]
+
+
+from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
+from mailman.interfaces.workflowstate import (
+ IWorkflowState, IWorkflowStateManager)
+from sqlalchemy import Column, Unicode
+from zope.interface import implementer
+
+
+
+@implementer(IWorkflowState)
+class WorkflowState(Model):
+ """Workflow states."""
+
+ __tablename__ = 'workflowstate'
+
+ name = Column(Unicode, primary_key=True)
+ key = Column(Unicode, primary_key=True)
+ step = Column(Unicode)
+ data = Column(Unicode)
+
+
+
+@implementer(IWorkflowStateManager)
+class WorkflowStateManager:
+ """See `IWorkflowStateManager`."""
+
+ @dbconnection
+ def save(self, store, name, key, step=None, data=None):
+ """See `IWorkflowStateManager`."""
+ state = store.query(WorkflowState).get((name, key))
+ if state is None:
+ state = store.add(WorkflowState(
+ name=name, key=key, step=step, data=data))
+ else:
+ state.step = step
+ state.data = data
+
+ @dbconnection
+ def restore(self, store, name, key):
+ """See `IWorkflowStateManager`."""
+ return store.query(WorkflowState).get((name, key))
diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py
index 0861a9a5b..9ec84da68 100644
--- a/src/mailman/rest/root.py
+++ b/src/mailman/rest/root.py
@@ -182,6 +182,7 @@ class TopLevel:
@child()
def lists(self, request, segments):
"""/<api>/lists
+ /<api>/lists/styles
/<api>/lists/<list>
/<api>/lists/<list>/...
"""
diff --git a/src/mailman/runners/docs/outgoing.rst b/src/mailman/runners/docs/outgoing.rst
index 8888aee5e..cc544d6eb 100644
--- a/src/mailman/runners/docs/outgoing.rst
+++ b/src/mailman/runners/docs/outgoing.rst
@@ -11,8 +11,8 @@ a *delivery module*, essentially a pluggable interface for determining how the
recipient set will be batched, whether messages will be personalized and
VERP'd, etc. The outgoing runner doesn't itself support retrying but it can
move messages to the 'retry queue' for handling delivery failures.
-::
+ >>> from mailman.testing.helpers import subscribe
>>> mlist = create_list('test@example.com')
>>> from mailman.testing.helpers import subscribe
diff --git a/src/mailman/styles/base.py b/src/mailman/styles/base.py
index 50cddbc32..7a77af609 100644
--- a/src/mailman/styles/base.py
+++ b/src/mailman/styles/base.py
@@ -41,7 +41,8 @@ from mailman.interfaces.archiver import ArchivePolicy
from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.digests import DigestFrequency
-from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
+from mailman.interfaces.mailinglist import (
+ Personalization, ReplyToMunging, SubscriptionPolicy)
from mailman.interfaces.nntp import NewsgroupModeration
@@ -75,6 +76,7 @@ class BasicOperation:
mlist.personalize = Personalization.none
mlist.default_member_action = Action.defer
mlist.default_nonmember_action = Action.hold
+ mlist.subscription_policy = SubscriptionPolicy.confirm
# Notify the administrator of pending requests and membership changes.
mlist.admin_immed_notify = True
mlist.admin_notify_mchanges = False
diff --git a/src/mailman/utilities/importer.py b/src/mailman/utilities/importer.py
index bb4273b12..66a23123c 100644
--- a/src/mailman/utilities/importer.py
+++ b/src/mailman/utilities/importer.py
@@ -41,6 +41,7 @@ from mailman.interfaces.digests import DigestFrequency
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.mailinglist import IAcceptableAliasSet
from mailman.interfaces.mailinglist import Personalization, ReplyToMunging
+from mailman.interfaces.mailinglist import SubscriptionPolicy
from mailman.interfaces.member import DeliveryMode, DeliveryStatus, MemberRole
from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.usermanager import IUserManager
@@ -178,6 +179,7 @@ TYPES = dict(
personalize=Personalization,
preferred_language=check_language_code,
reply_goes_to_list=ReplyToMunging,
+ subscription_policy=SubscriptionPolicy,
topics_enabled=bool,
)
@@ -202,6 +204,7 @@ NAME_MAPPINGS = dict(
real_name='display_name',
send_goodbye_msg='send_goodbye_message',
send_welcome_msg='send_welcome_message',
+ subscribe_policy='subscription_policy',
)
# These DateTime fields of the mailinglist table need a type conversion to
diff --git a/src/mailman/utilities/tests/test_import.py b/src/mailman/utilities/tests/test_import.py
index 938ef7d2e..59387dfa9 100644
--- a/src/mailman/utilities/tests/test_import.py
+++ b/src/mailman/utilities/tests/test_import.py
@@ -44,7 +44,8 @@ from mailman.interfaces.autorespond import ResponseAction
from mailman.interfaces.bans import IBanManager
from mailman.interfaces.bounce import UnrecognizedBounceDisposition
from mailman.interfaces.languages import ILanguageManager
-from mailman.interfaces.mailinglist import IAcceptableAliasSet
+from mailman.interfaces.mailinglist import (
+ IAcceptableAliasSet, SubscriptionPolicy)
from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.interfaces.nntp import NewsgroupModeration
from mailman.interfaces.templates import ITemplateLoader
@@ -301,6 +302,30 @@ class TestBasicImport(unittest.TestCase):
self._import()
self.assertEqual(self._mlist.encode_ascii_prefixes, True)
+ def test_subscription_policy_open(self):
+ self._mlist.subscription_policy = SubscriptionPolicy.confirm
+ self._pckdict['subscribe_policy'] = 0
+ self._import()
+ self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.open)
+
+ def test_subscription_policy_confirm(self):
+ self._mlist.subscription_policy = SubscriptionPolicy.open
+ self._pckdict['subscribe_policy'] = 1
+ self._import()
+ self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.confirm)
+
+ def test_subscription_policy_moderate(self):
+ self._mlist.subscription_policy = SubscriptionPolicy.open
+ self._pckdict['subscribe_policy'] = 2
+ self._import()
+ self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.moderate)
+
+ def test_subscription_policy_confirm_then_moderate(self):
+ self._mlist.subscription_policy = SubscriptionPolicy.open
+ self._pckdict['subscribe_policy'] = 3
+ self._import()
+ self.assertEqual(self._mlist.subscription_policy, SubscriptionPolicy.confirm_then_moderate)
+
class TestArchiveImport(unittest.TestCase):