From 8b09113eb40c39ada3dc902cb4e869c8f012c97d Mon Sep 17 00:00:00 2001
From: J08nY
Date: Thu, 29 Jun 2017 23:51:47 +0200
Subject: Create mailman.workflows package. Move base Workflow there.
- Also introduce IWorkflow, ISubscriptionWorkflow,
IUnsubscriptionWorkflow.
---
src/mailman/app/subscriptions.py | 4 +-
src/mailman/app/tests/test_workflow.py | 12 +--
src/mailman/app/workflow.py | 151 -------------------------------
src/mailman/config/configure.zcml | 4 +-
src/mailman/interfaces/workflow.py | 67 --------------
src/mailman/interfaces/workflows.py | 128 ++++++++++++++++++++++++++
src/mailman/model/tests/test_workflow.py | 2 +-
src/mailman/model/workflow.py | 71 ---------------
src/mailman/model/workflows.py | 71 +++++++++++++++
src/mailman/workflows/__init__.py | 0
src/mailman/workflows/base.py | 143 +++++++++++++++++++++++++++++
11 files changed, 353 insertions(+), 300 deletions(-)
delete mode 100644 src/mailman/app/workflow.py
delete mode 100644 src/mailman/interfaces/workflow.py
create mode 100644 src/mailman/interfaces/workflows.py
delete mode 100644 src/mailman/model/workflow.py
create mode 100644 src/mailman/model/workflows.py
create mode 100644 src/mailman/workflows/__init__.py
create mode 100644 src/mailman/workflows/base.py
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index 0d8176ffb..dace9ccb6 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -24,7 +24,6 @@ from datetime import timedelta
from email.utils import formataddr
from enum import Enum
from mailman.app.membership import delete_member
-from mailman.app.workflow import Workflow
from mailman.core.i18n import _
from mailman.database.transaction import flush
from mailman.email.message import UserNotification
@@ -43,9 +42,10 @@ from mailman.interfaces.subscriptions import (
from mailman.interfaces.template import ITemplateLoader
from mailman.interfaces.user import IUser
from mailman.interfaces.usermanager import IUserManager
-from mailman.interfaces.workflow import IWorkflowStateManager
+from mailman.interfaces.workflows import IWorkflowStateManager
from mailman.utilities.datetime import now
from mailman.utilities.string import expand, wrap
+from mailman.workflows.base import Workflow
from public import public
from zope.component import getUtility
from zope.event import notify
diff --git a/src/mailman/app/tests/test_workflow.py b/src/mailman/app/tests/test_workflow.py
index e82001540..f242a73ec 100644
--- a/src/mailman/app/tests/test_workflow.py
+++ b/src/mailman/app/tests/test_workflow.py
@@ -20,15 +20,15 @@
import json
import unittest
-from mailman.app.workflow import Workflow
-from mailman.interfaces.workflow import IWorkflowStateManager
+from mailman.interfaces.workflows import IWorkflowStateManager
from mailman.testing.layers import ConfigLayer
+from mailman.workflows.base import Workflow
from zope.component import getUtility
class MyWorkflow(Workflow):
- INITIAL_STATE = 'first'
- SAVE_ATTRIBUTES = ('ant', 'bee', 'cat')
+ initial_state = 'first'
+ save_attributes = ('ant', 'bee', 'cat')
def __init__(self):
super().__init__()
@@ -51,7 +51,7 @@ class MyWorkflow(Workflow):
class DependentWorkflow(MyWorkflow):
- SAVE_ATTRIBUTES = ('ant', 'bee', 'cat', 'elf')
+ save_attributes = ('ant', 'bee', 'cat', 'elf')
def __init__(self):
super().__init__()
@@ -136,7 +136,7 @@ class TestWorkflow(unittest.TestCase):
def test_save_and_restore_dependant_attributes(self):
# Attributes must be restored in the order they are declared in
- # SAVE_ATTRIBUTES.
+ # save_attributes.
workflow = iter(DependentWorkflow())
workflow.elf = 6
workflow.save()
diff --git a/src/mailman/app/workflow.py b/src/mailman/app/workflow.py
deleted file mode 100644
index 92b78c184..000000000
--- a/src/mailman/app/workflow.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# Copyright (C) 2015-2017 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 .
-
-"""Generic workflow."""
-
-import sys
-import json
-import logging
-
-from collections import deque
-from mailman.interfaces.workflow import IWorkflowStateManager
-from public import public
-from zope.component import getUtility
-
-
-COMMASPACE = ', '
-log = logging.getLogger('mailman.error')
-
-
-@public
-class Workflow:
- """Generic workflow."""
-
- SAVE_ATTRIBUTES = ()
- INITIAL_STATE = None
-
- def __init__(self):
- self.token = None
- self._next = deque()
- self.push(self.INITIAL_STATE)
- self.debug = False
- self._count = 0
-
- @property
- def name(self):
- return self.__class__.__name__
-
- def __iter__(self):
- return self
-
- def push(self, step):
- self._next.append(step)
-
- def _pop(self):
- name = self._next.popleft()
- step = getattr(self, '_step_{}'.format(name))
- self._count += 1
- if self.debug: # pragma: nocover
- print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr)
- return name, step
-
- def __next__(self):
- try:
- name, step = self._pop()
- return step()
- except IndexError:
- raise StopIteration
- except:
- log.exception('deque: {}'.format(COMMASPACE.join(self._next)))
- raise
-
- def run_thru(self, stop_after):
- """Run the state machine through and including the given step.
-
- :param stop_after: Name of method, sans prefix to run the
- state machine through. In other words, the state machine runs
- until the named method completes.
- """
- results = []
- while True:
- try:
- name, step = self._pop()
- except (StopIteration, IndexError):
- # We're done.
- break
- results.append(step())
- if name == stop_after:
- break
- return results
-
- def run_until(self, stop_before):
- """Trun the state machine until (not including) the given step.
-
- :param stop_before: Name of method, sans prefix that the
- state machine is run until the method is reached. Unlike
- `run_thru()` the named method is not run.
- """
- results = []
- while True:
- try:
- name, step = self._pop()
- except (StopIteration, IndexError):
- # We're done.
- break
- if name == stop_before:
- # Stop executing, but not before we push the last state back
- # onto the deque. Otherwise, resuming the state machine would
- # skip this step.
- self._next.appendleft(name)
- break
- results.append(step())
- return results
-
- def save(self):
- assert self.token, 'Workflow token must be set'
- 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. This is not
- # an issue in practice, 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() 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.token, step, json.dumps(data))
-
- def restore(self):
- state_manager = getUtility(IWorkflowStateManager)
- state = state_manager.restore(self.token)
- if state is None:
- # The token doesn't exist in the database.
- raise LookupError(self.token)
- self._next.clear()
- if state.step:
- self._next.append(state.step)
- data = json.loads(state.data)
- for attr in self.SAVE_ATTRIBUTES:
- try:
- setattr(self, attr, data[attr])
- except KeyError:
- pass
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index 2fd0c8788..da4eb36b6 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -135,8 +135,8 @@
/>
diff --git a/src/mailman/interfaces/workflow.py b/src/mailman/interfaces/workflow.py
deleted file mode 100644
index 5b3582b58..000000000
--- a/src/mailman/interfaces/workflow.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Copyright (C) 2015-2017 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 .
-
-"""Interfaces describing the state of a workflow."""
-
-from public import public
-from zope.interface import Attribute, Interface
-
-
-@public
-class IWorkflowState(Interface):
- """The state of a workflow."""
-
- token = Attribute('A unique key identifying the workflow instance.')
-
- step = Attribute("This workflow's next step.")
-
- data = Attribute('Additional data (may be JSON-encoded).')
-
-
-@public
-class IWorkflowStateManager(Interface):
- """The workflow states manager."""
-
- def save(token, step, data=None):
- """Save the state of a workflow.
-
- :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(token):
- """Get the saved state for a workflow or None if nothing was saved.
-
- :param token: A unique token identifying this workflow instance.
- :type token: str
- :return: The saved state associated with this name/token pair, or None
- if the pair isn't in the database.
- :rtype: ``IWorkflowState``
- """
-
- def discard(token):
- """Throw away the saved state for a workflow.
-
- :param token: A unique token identifying this workflow instance.
- :type token: str
- """
-
- count = Attribute('The number of saved workflows in the database.')
diff --git a/src/mailman/interfaces/workflows.py b/src/mailman/interfaces/workflows.py
new file mode 100644
index 000000000..b110a481e
--- /dev/null
+++ b/src/mailman/interfaces/workflows.py
@@ -0,0 +1,128 @@
+# Copyright (C) 2015-2017 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 .
+
+"""Interfaces describing the a workflow, it's state and it's state manager."""
+
+from public import public
+from zope.interface import Attribute, Interface
+
+
+@public
+class IWorkflowState(Interface):
+ """The state of a workflow."""
+
+ token = Attribute('A unique key identifying the workflow instance.')
+
+ step = Attribute("This workflow's next step.")
+
+ data = Attribute('Additional data (may be JSON-encoded).')
+
+
+@public
+class IWorkflowStateManager(Interface):
+ """The workflow states manager."""
+
+ def save(token, step, data=None):
+ """Save the state of a workflow.
+
+ :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(token):
+ """Get the saved state for a workflow or None if nothing was saved.
+
+ :param token: A unique token identifying this workflow instance.
+ :type token: str
+ :return: The saved state associated with this name/token pair, or None
+ if the pair isn't in the database.
+ :rtype: ``IWorkflowState``
+ """
+
+ def discard(token):
+ """Throw away the saved state for a workflow.
+
+ :param token: A unique token identifying this workflow instance.
+ :type token: str
+ """
+
+ count = Attribute('The number of saved workflows in the database.')
+
+
+@public
+class IWorkflow(Interface):
+ """A workflow."""
+
+ name = Attribute('The name of the workflow, must be unique.')
+ description = Attribute('A brief description of the workflow.')
+ initial_state = Attribute('The state in which the workflow starts.')
+ save_attributes = Attribute('The sequence of attributes of the workflow, '
+ 'which are saved.')
+
+ def __iter__():
+ """Return an iterator over the steps."""
+
+ def __next__():
+ """Run the next step from the queue.
+
+ :return: The result of the step run.
+ """
+
+ def push(step):
+ """Push a step to this workflows queue."""
+
+ def run_thru(stop_after):
+ """Run the state machine through and including the given step.
+
+ :param stop_after: Name of method, sans prefix to run the
+ state machine through. In other words, the state machine runs
+ until the named method completes.
+ """
+
+ def run_until(stop_before):
+ """Run the state machine until (not including) the given step.
+
+ :param stop_before: Name of method, sans prefix that the
+ state machine is run until the method is reached. Unlike
+ `run_thru()` the named method is not run.
+ """
+
+ def save():
+ """Save the workflow in it's current state.
+
+ Needs to have the `token` attribute set.
+ """
+
+ def restore():
+ """Restore the workflow from the database.
+
+ Needs to have the `token` attribute set.
+ """
+
+
+@public
+class ISubscriptionWorkflow(IWorkflow):
+ """A workflow used for subscription."""
+
+
+@public
+class IUnsubscriptionWorkflow(IWorkflow):
+ """A workflow used for unsubscription."""
diff --git a/src/mailman/model/tests/test_workflow.py b/src/mailman/model/tests/test_workflow.py
index 9cc5446b7..41c00efc0 100644
--- a/src/mailman/model/tests/test_workflow.py
+++ b/src/mailman/model/tests/test_workflow.py
@@ -19,7 +19,7 @@
import unittest
-from mailman.interfaces.workflow import IWorkflowStateManager
+from mailman.interfaces.workflows import IWorkflowStateManager
from mailman.testing.layers import ConfigLayer
from zope.component import getUtility
diff --git a/src/mailman/model/workflow.py b/src/mailman/model/workflow.py
deleted file mode 100644
index 3ca3412a6..000000000
--- a/src/mailman/model/workflow.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# Copyright (C) 2015-2017 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 .
-
-"""Model for workflow states."""
-
-from mailman.database.model import Model
-from mailman.database.transaction import dbconnection
-from mailman.database.types import SAUnicode
-from mailman.interfaces.workflow import IWorkflowState, IWorkflowStateManager
-from public import public
-from sqlalchemy import Column
-from zope.interface import implementer
-
-
-@public
-@implementer(IWorkflowState)
-class WorkflowState(Model):
- """Workflow states."""
-
- __tablename__ = 'workflowstate'
-
- token = Column(SAUnicode, primary_key=True)
- step = Column(SAUnicode)
- data = Column(SAUnicode)
-
-
-@public
-@implementer(IWorkflowStateManager)
-class WorkflowStateManager:
- """See `IWorkflowStateManager`."""
-
- @dbconnection
- def save(self, store, token, step=None, data=None):
- """See `IWorkflowStateManager`."""
- state = WorkflowState(token=token, step=step, data=data)
- store.add(state)
-
- @dbconnection
- def restore(self, store, token):
- """See `IWorkflowStateManager`."""
- state = store.query(WorkflowState).get(token)
- if state is not None:
- store.delete(state)
- return state
-
- @dbconnection
- def discard(self, store, token):
- """See `IWorkflowStateManager`."""
- state = store.query(WorkflowState).get(token)
- if state is not None:
- store.delete(state)
-
- @property
- @dbconnection
- def count(self, store):
- """See `IWorkflowStateManager`."""
- return store.query(WorkflowState).count()
diff --git a/src/mailman/model/workflows.py b/src/mailman/model/workflows.py
new file mode 100644
index 000000000..587d3375e
--- /dev/null
+++ b/src/mailman/model/workflows.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2015-2017 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 .
+
+"""Model for workflow states."""
+
+from mailman.database.model import Model
+from mailman.database.transaction import dbconnection
+from mailman.database.types import SAUnicode
+from mailman.interfaces.workflows import IWorkflowState, IWorkflowStateManager
+from public import public
+from sqlalchemy import Column
+from zope.interface import implementer
+
+
+@public
+@implementer(IWorkflowState)
+class WorkflowState(Model):
+ """Workflow states."""
+
+ __tablename__ = 'workflowstate'
+
+ token = Column(SAUnicode, primary_key=True)
+ step = Column(SAUnicode)
+ data = Column(SAUnicode)
+
+
+@public
+@implementer(IWorkflowStateManager)
+class WorkflowStateManager:
+ """See `IWorkflowStateManager`."""
+
+ @dbconnection
+ def save(self, store, token, step=None, data=None):
+ """See `IWorkflowStateManager`."""
+ state = WorkflowState(token=token, step=step, data=data)
+ store.add(state)
+
+ @dbconnection
+ def restore(self, store, token):
+ """See `IWorkflowStateManager`."""
+ state = store.query(WorkflowState).get(token)
+ if state is not None:
+ store.delete(state)
+ return state
+
+ @dbconnection
+ def discard(self, store, token):
+ """See `IWorkflowStateManager`."""
+ state = store.query(WorkflowState).get(token)
+ if state is not None:
+ store.delete(state)
+
+ @property
+ @dbconnection
+ def count(self, store):
+ """See `IWorkflowStateManager`."""
+ return store.query(WorkflowState).count()
diff --git a/src/mailman/workflows/__init__.py b/src/mailman/workflows/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/mailman/workflows/base.py b/src/mailman/workflows/base.py
new file mode 100644
index 000000000..5988dfa43
--- /dev/null
+++ b/src/mailman/workflows/base.py
@@ -0,0 +1,143 @@
+# Copyright (C) 2015-2017 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 .
+
+"""Generic workflow."""
+
+import sys
+import json
+import logging
+
+from collections import deque
+
+from mailman.interfaces.workflows import IWorkflowStateManager
+from public import public
+from zope.component import getUtility
+
+
+COMMASPACE = ', '
+log = logging.getLogger('mailman.error')
+
+
+@public
+class Workflow:
+ """Generic workflow."""
+
+ initial_state = None
+ save_attributes = ()
+
+ def __init__(self):
+ self.token = None
+ self._next = deque()
+ self.push(self.initial_state)
+ self.debug = False
+ self._count = 0
+
+ def __iter__(self):
+ """See `IWorkflow`."""
+ return self
+
+ def __next__(self):
+ """See `IWorkflow`."""
+ try:
+ name, step = self._pop()
+ return step()
+ except IndexError:
+ raise StopIteration
+ except:
+ log.exception('deque: {}'.format(COMMASPACE.join(self._next)))
+ raise
+
+ def push(self, step):
+ """See `IWorkflow`."""
+ self._next.append(step)
+
+ def _pop(self):
+ name = self._next.popleft()
+ step = getattr(self, '_step_{}'.format(name))
+ self._count += 1
+ if self.debug: # pragma: nocover
+ print('[{:02d}] -> {}'.format(self._count, name), file=sys.stderr)
+ return name, step
+
+ def run_thru(self, stop_after):
+ """See `IWorkflow`."""
+ results = []
+ while True:
+ try:
+ name, step = self._pop()
+ except (StopIteration, IndexError):
+ # We're done.
+ break
+ results.append(step())
+ if name == stop_after:
+ break
+ return results
+
+ def run_until(self, stop_before):
+ """See `IWorkflow`."""
+ results = []
+ while True:
+ try:
+ name, step = self._pop()
+ except (StopIteration, IndexError):
+ # We're done.
+ break
+ if name == stop_before:
+ # Stop executing, but not before we push the last state back
+ # onto the deque. Otherwise, resuming the state machine would
+ # skip this step.
+ self._next.appendleft(name)
+ break
+ results.append(step())
+ return results
+
+ def save(self):
+ """See `IWorkflow`."""
+ assert self.token, 'Workflow token must be set'
+ 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. This is not
+ # an issue in practice, 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() 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.token, step, json.dumps(data))
+
+ def restore(self):
+ """See `IWorkflow`."""
+ state_manager = getUtility(IWorkflowStateManager)
+ state = state_manager.restore(self.token)
+ if state is None:
+ # The token doesn't exist in the database.
+ raise LookupError(self.token)
+ self._next.clear()
+ if state.step:
+ self._next.append(state.step)
+ data = json.loads(state.data)
+ for attr in self.save_attributes:
+ try:
+ setattr(self, attr, data[attr])
+ except KeyError:
+ pass
--
cgit v1.2.3-70-g09d2