extend 'stateful' implementation (sort of a very simple workflow system)

git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@1933 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2007-08-19 14:41:23 +00:00
parent 6bd2c94f05
commit c0c772ff30
4 changed files with 199 additions and 36 deletions

View file

@ -1,16 +1,99 @@
================ ================
Stateful objects Stateful Objects
================ ================
($Id$) ($Id$)
>>> from cybertools.stateful.definition import StatesDefinition >>> from cybertools.stateful.definition import StatesDefinition
>>> from cybertools.stateful.definition import State, Transition >>> from cybertools.stateful.definition import State, Transition
>>> from cybertools.stateful.definition import registerStatesDefinition
>>> from cybertools.stateful.base import Stateful >>> from cybertools.stateful.base import Stateful
We start with a simple demonstration class that provides stateful
behaviour directly.
>>> class Demo(Stateful): >>> class Demo(Stateful):
... pass ... pass
>>> demo = Demo() >>> demo = Demo()
The default states definition has the `started` state has its initial
state.
>>> demo.getState() >>> demo.getState()
'started' 'started'
>>> demo.getStateObject().title
'Started'
We can now execute the `finish` Transition.
>>> demo.doTransition('finish')
>>> demo.getState()
'finished'
More complex states definitions
-------------------------------
>>> registerStatesDefinition(
... StatesDefinition('publishing',
... State('private', 'private', ('show',)),
... State('visible', 'visible', ('publish', 'hide',)),
... State('published', 'published', ('retract',)),
... Transition('show', 'show', 'visible'),
... Transition('hide', 'hide', 'private'),
... Transition('publish', 'publish', 'published'),
... Transition('retract', 'retract', 'visible'),
... initialState='visible'))
>>> demo = Demo()
>>> demo.statesDefinition = 'publishing'
>>> demo.getState()
'visible'
If we try to execute a transition that is not an outgoing transition
of the current state we get an error.
>>> demo.doTransition('retract')
Traceback (most recent call last):
...
ValueError: Transition 'retract' is not reachable from state 'visible'.
>>> demo.getState()
'visible'
Stateful Adapters
=================
Objects that show stateful behaviour need not be derived from the Stateful
class, for persistent objects one can also provide a stateful adapter.
>>> from persistent import Persistent
>>> class Demo(Persistent):
... pass
>>> demo = Demo()
>>> from zope import component
>>> from cybertools.stateful.base import StatefulAdapter
>>> component.provideAdapter(StatefulAdapter)
We can now retrieve a stateful adapter using the IStateful interface.
>>> from cybertools.stateful.interfaces import IStateful
>>> statefulDemo = IStateful(demo)
>>> statefulDemo.getState()
'started'
>>> statefulDemo.getStateObject().title
'Started'
>>> statefulDemo.doTransition('finish')
>>> statefulDemo.getState()
'finished'
If we make a new adapter for the same persistent object we get
back the state that is stored with the object.
>>> statefulDemo = IStateful(demo)
>>> statefulDemo.getState()
'finished'

View file

@ -17,28 +17,35 @@
# #
""" """
State definition implementation. Basic implementations for stateful objects and adapters.
$Id$ $Id$
""" """
from persistent.interfaces import IPersistent
from persistent.mapping import PersistentMapping
from zope.component import adapts
from zope.interface import implements from zope.interface import implements
from cybertools.stateful.interfaces import IStateful from cybertools.stateful.interfaces import IStateful
from cybertools.stateful.definition import statesDefinitions from cybertools.stateful.definition import statesDefinitions
class Stateful: class Stateful(object):
implements(IStateful) implements(IStateful)
_statesDefinition = 'default' statesDefinition = 'default'
_state = None state = None
def getState(self): def getState(self):
if self._state is None: if self.state is None:
self._state = self.getStatesDefinition()._initialState self.state = self.getStatesDefinition().initialState
return self._state return self.state
def getStateObject(self):
state = self.getState()
return self.getStatesDefinition().states[state]
def doTransition(self, transition): def doTransition(self, transition):
""" execute transition. """ execute transition.
@ -51,6 +58,29 @@ class Stateful:
return sd.getAvailableTransitionsFor(self) return sd.getAvailableTransitionsFor(self)
def getStatesDefinition(self): def getStatesDefinition(self):
return statesDefinitions.get(self._statesDefinition, None) return statesDefinitions.get(self.statesDefinition, None)
class StatefulAdapter(Stateful):
""" An adapter for persistent objects to make the stateful.
"""
adapts(IPersistent)
statesAttributeName = '__states__'
def __init__(self, context):
self.context = context
def getState(self):
statesAttr = getattr(self.context, self.statesAttributeName, {})
return statesAttr.get(self.statesDefinition,
self.getStatesDefinition().initialState)
def setState(self, value):
statesAttr = getattr(self.context, self.statesAttributeName, None)
if statesAttr is None:
statesAttr = PersistentMapping()
setattr(self.context, self.statesAttributeName, statesAttr)
statesAttr[self.statesDefinition] = value
state = property(getState, setState)

View file

@ -23,22 +23,28 @@ $Id$
""" """
from zope.interface import implements from zope.interface import implements
from cybertools.util.jeep import Jeep
from cybertools.stateful.interfaces import IState, ITransition
from cybertools.stateful.interfaces import IStatesDefinition from cybertools.stateful.interfaces import IStatesDefinition
class State(object): class State(object):
def __init__(self, id, title, transitions): implements(IState)
self.id = id
def __init__(self, name, title, transitions):
self.name = self.__name__ = name
self.title = title self.title = title
self.transitions = transitions self.transitions = transitions
class Transition(object): class Transition(object):
def __init__(self, id, title, targetState): implements(ITransition)
self.id = id
def __init__(self, name, title, targetState):
self.name = self.__name__ = name
self.title = title self.title = title
self.targetState = targetState self.targetState = targetState
@ -47,27 +53,49 @@ class StatesDefinition(object):
implements(IStatesDefinition) implements(IStatesDefinition)
# Basic/example states definition: initialState = 'started'
_states = {
'started': State('started', 'Started', ('finish',)),
'finished': State('finished', 'Finished', ()),
}
_transitions = {
'finish': Transition('finish', 'Finish', 'finished')
}
_initialState = 'started'
def doTransitionFor(self, object, transition): def __init__(self, name, *details, **kw):
object._state = self._transitions[transition].targetState self.name = self.__name__ = name
self.states = Jeep()
self.transitions = Jeep()
for d in details:
if ITransition.providedBy(d):
self.transitions.append(d)
elif IState.providedBy(d):
self.states.append(d)
else:
raise TypeError('Only states or transitions are allowed here, '
'got %s instead.' % repr(d))
for k, v in kw.items():
setattr(self, k, v)
def getAvailableTransitionsFor(self, object): def doTransitionFor(self, obj, transition):
state = object.getState() if transition not in self.transitions:
return [ self._transitions[t] for t in self._states[state].transitions ] raise ValueError('Transition %s is not available.' % transition)
if transition not in [t.name for t in self.getAvailableTransitionsFor(obj)]:
raise ValueError("Transition '%s' is not reachable from state '%s'."
% (transition, obj.getState()))
obj.state = self.transitions[transition].targetState
def getAvailableTransitionsFor(self, obj):
state = obj.getState()
return [self.transitions[t] for t in self.states[state].transitions]
# dummy default states definition
defaultSD = StatesDefinition('default',
State('started', 'Started', ('finish',)),
State('finished', 'Finished', ()),
Transition('finish', 'Finish', 'finished'),
)
def registerStatesDefinition(id, definition): # states definitions registry
statesDefinitions[id] = definition
statesDefinitions = { statesDefinitions = dict()
'default': StatesDefinition(),
} def registerStatesDefinition(definition):
statesDefinitions[definition.name] = definition
registerStatesDefinition(defaultSD)

View file

@ -22,7 +22,23 @@ Interfaces for the `stateful` package.
$Id$ $Id$
""" """
from zope.interface import Interface from zope.interface import Interface, Attribute
class IState(Interface):
name = Attribute('The name or identifier of the state')
title = Attribute('A user-readable name or title of the state')
transitions = Attribute('A sequence of strings naming the transitions '
'that can be executed from this state')
class ITransition(Interface):
name = Attribute('The name or identifier of the transition')
title = Attribute('A user-readable name or title of the transition')
targetState = Attribute('A string naming the state that will be the '
'result of executing this transition')
class IStateful(Interface): class IStateful(Interface):
@ -30,11 +46,15 @@ class IStateful(Interface):
""" """
def getState(): def getState():
""" Return the workflow state of the object. """ Return the name of the state of the object.
"""
def getStateObject():
""" Return the state (an IState implementation) of the object.
""" """
def doTransition(transition): def doTransition(transition):
""" Execute a transition; the transition is specified by its id. """ Execute a transition; the transition is specified by its name.
""" """
def getAvailableTransitions(): def getAvailableTransitions():
@ -50,9 +70,11 @@ class IStatesDefinition(Interface):
Similar to an entity-based workflow definition. Similar to an entity-based workflow definition.
""" """
name = Attribute('The name or identifier of the states definition')
def doTransitionFor(object, transition): def doTransitionFor(object, transition):
""" Execute a transition for the object given; """ Execute a transition for the object given;
the transition is specified by its id. the transition is specified by its name.
""" """
def getAvailableTransitionsFor(object): def getAvailableTransitionsFor(object):