From 9de0ea02dc757e41a5013bb41e68d04f44ed5066 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 29 Mar 2015 17:14:09 -0400 Subject: Refactorings and tests. * Move the basic Workflow class to a module in mailman.app. * Rename the interface and model modules. * Update the configure.zcml. * Minor style fixes. * Add a test for the workflow model. --- src/mailman/app/subscriptions.py | 76 ++------------ src/mailman/app/workflow.py | 91 +++++++++++++++++ src/mailman/config/configure.zcml | 4 +- .../versions/2bb9b382198_workflow_state_table.py | 2 +- src/mailman/interfaces/workflow.py | 66 +++++++++++++ src/mailman/interfaces/workflowstate.py | 70 ------------- src/mailman/model/tests/test_workflow.py | 110 +++++++++++++++++++++ src/mailman/model/workflow.py | 65 ++++++++++++ src/mailman/model/workflowstate.py | 66 ------------- 9 files changed, 341 insertions(+), 209 deletions(-) create mode 100644 src/mailman/app/workflow.py create mode 100644 src/mailman/interfaces/workflow.py delete mode 100644 src/mailman/interfaces/workflowstate.py create mode 100644 src/mailman/model/tests/test_workflow.py create mode 100644 src/mailman/model/workflow.py delete mode 100644 src/mailman/model/workflowstate.py diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py index 1e45da43e..8cd507bd2 100644 --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -24,16 +24,10 @@ __all__ = [ ] -import json -from collections import deque -from operator import attrgetter -from sqlalchemy import and_, or_ -from uuid import UUID -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.app.workflow import Workflow from mailman.core.constants import system_preferences from mailman.database.transaction import dbconnection from mailman.interfaces.address import IAddress @@ -45,9 +39,13 @@ 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 +from operator import attrgetter +from sqlalchemy import and_, or_ +from uuid import UUID +from zope.component import getUtility +from zope.interface import implementer def _membership_sort_key(member): @@ -59,68 +57,6 @@ 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.""" diff --git a/src/mailman/app/workflow.py b/src/mailman/app/workflow.py new file mode 100644 index 000000000..91c8c9f84 --- /dev/null +++ b/src/mailman/app/workflow.py @@ -0,0 +1,91 @@ +# Copyright (C) 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 . + +"""Generic workflow.""" + +__all__ = [ + 'Workflow', + ] + + +import json + +from collections import deque +from mailman.interfaces.workflow import IWorkflowStateManager +from zope.component import getUtility + + + +class Workflow: + """Generic workflow.""" + + _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) diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml index f6cb6dac1..1f2283b02 100644 --- a/src/mailman/config/configure.zcml +++ b/src/mailman/config/configure.zcml @@ -118,8 +118,8 @@ /> diff --git a/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py b/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py index e6608b6ce..59cb1121e 100644 --- a/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py +++ b/src/mailman/database/alembic/versions/2bb9b382198_workflow_state_table.py @@ -21,7 +21,7 @@ def upgrade(): sa.Column('step', sa.Unicode(), nullable=True), sa.Column('data', sa.Unicode(), nullable=True), sa.PrimaryKeyConstraint('name', 'key') - ) + ) def downgrade(): diff --git a/src/mailman/interfaces/workflow.py b/src/mailman/interfaces/workflow.py new file mode 100644 index 000000000..b5aeec093 --- /dev/null +++ b/src/mailman/interfaces/workflow.py @@ -0,0 +1,66 @@ +# Copyright (C) 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 . + +"""Interfaces describing the state of a workflow.""" + +__all__ = [ + 'IWorkflowState', + 'IWorkflowStateManager', + ] + + +from zope.interface import Attribute, Interface + + + +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/interfaces/workflowstate.py b/src/mailman/interfaces/workflowstate.py deleted file mode 100644 index 186386170..000000000 --- a/src/mailman/interfaces/workflowstate.py +++ /dev/null @@ -1,70 +0,0 @@ -# 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 . - -"""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/tests/test_workflow.py b/src/mailman/model/tests/test_workflow.py new file mode 100644 index 000000000..b5e915df4 --- /dev/null +++ b/src/mailman/model/tests/test_workflow.py @@ -0,0 +1,110 @@ +# Copyright (C) 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 . + +"""Test the workflow model.""" + +__all__ = [ + 'TestWorkflow', + ] + + +import unittest + +from mailman.interfaces.workflow import IWorkflowStateManager +from mailman.testing.layers import ConfigLayer +from zope.component import getUtility + + + +class TestWorkflow(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._manager = getUtility(IWorkflowStateManager) + + def test_save_restore_workflow(self): + # Save and restore a workflow. + name = 'ant' + key = 'bee' + step = 'cat' + data = 'dog' + self._manager.save(name, key, step, data) + workflow = self._manager.restore(name, key) + self.assertEqual(workflow.name, name) + self.assertEqual(workflow.key, key) + 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' + data = 'dog' + self._manager.save(name, key, data=data) + workflow = self._manager.restore(name, key) + self.assertEqual(workflow.name, name) + self.assertEqual(workflow.key, key) + 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' + step = 'cat' + self._manager.save(name, key, step) + workflow = self._manager.restore(name, key) + self.assertEqual(workflow.name, name) + self.assertEqual(workflow.key, key) + 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) + self.assertEqual(workflow.name, name) + self.assertEqual(workflow.key, key) + 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) + 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. + name = 'ant' + key = 'bee' + self._manager.save(name, key) + 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 + # database. + name = 'ant' + key = 'bee' + self._manager.save(name, key) + workflow = self._manager.restore('ewe', 'fly') + self.assertIsNone(workflow) diff --git a/src/mailman/model/workflow.py b/src/mailman/model/workflow.py new file mode 100644 index 000000000..53c9f05ea --- /dev/null +++ b/src/mailman/model/workflow.py @@ -0,0 +1,65 @@ +# Copyright (C) 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 . + +"""Model for workflow states.""" + +__all__ = [ + 'WorkflowState', + 'WorkflowStateManager', + ] + + +from mailman.database.model import Model +from mailman.database.transaction import dbconnection +from mailman.interfaces.workflow 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 = WorkflowState(name=name, key=key, step=step, data=data) + store.add(state) + 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/model/workflowstate.py b/src/mailman/model/workflowstate.py deleted file mode 100644 index 229a2240b..000000000 --- a/src/mailman/model/workflowstate.py +++ /dev/null @@ -1,66 +0,0 @@ -# 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 . - -"""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)) -- cgit v1.2.3-70-g09d2