diff --git a/stateful/base.py b/stateful/base.py index 16511b8..3d64858 100644 --- a/stateful/base.py +++ b/stateful/base.py @@ -142,5 +142,3 @@ class TransitionEvent(ObjectEvent): self.transition = transition self.previousState = previousState self.request = request - - diff --git a/stateful/definition.py b/stateful/definition.py index 8cd2b2e..4930e52 100644 --- a/stateful/definition.py +++ b/stateful/definition.py @@ -25,7 +25,7 @@ $Id$ from zope.interface import implements from cybertools.util.jeep import Jeep -from cybertools.stateful.interfaces import IState, ITransition +from cybertools.stateful.interfaces import IState, IAction, ITransition from cybertools.stateful.interfaces import IStatesDefinition @@ -33,7 +33,7 @@ class State(object): implements(IState) - security = lambda context: None + setSecurity = lambda self, context: None icon = None color = 'blue' @@ -41,20 +41,37 @@ class State(object): self.name = self.__name__ = name self.title = title self.transitions = transitions + self.actions = Jeep(kw.pop('actions', [])) for k, v in kw.items(): setattr(self, k, v) -class Transition(object): +class Action(object): + + implements(IAction) + + allowed = True + permission = None + roles = [] + + def __init__(self, name, title=None, **kw): + self.name = self.__name__ = name + self.title = title + for k, v in kw.items(): + setattr(self, k, v) + + @staticmethod + def doBefore(context): + return None + + +class Transition(Action): implements(ITransition) def __init__(self, name, title, targetState, **kw): - self.name = self.__name__ = name - self.title = title + super(Transition, self).__init__(name, title, **kw) self.targetState = targetState - for k, v in kw.items(): - setattr(self, k, v) class StatesDefinition(object): @@ -81,14 +98,36 @@ class StatesDefinition(object): def doTransitionFor(self, obj, transition): if transition not in self.transitions: raise ValueError('Transition %s is not available.' % transition) - if transition not in [t.name for t in self.getAvailableTransitionsFor(obj)]: + trans = self.transitions[transition] + if not self.isAllowed(trans, obj): + raise ValueError('Transition %s is not allowed.' % transition) + if trans not in self.getAvailableTransitionsFor(obj): raise ValueError("Transition '%s' is not reachable from state '%s'." % (transition, obj.getState())) - obj.state = self.transitions[transition].targetState + trans.doBefore(obj) + obj.state = trans.targetState + obj.getStateObject().setSecurity(obj) def getAvailableTransitionsFor(self, obj): state = obj.getState() - return [self.transitions[t] for t in self.states[state].transitions] + return [self.transitions[t] + for t in self.states[state].transitions + if self.isAllowed(self.transitions[t], obj)] + + def isAllowed(self, action, obj): + if not action.allowed: + return False + if not self.checkRoles(action.roles, obj): + return False + if not self.checkPermission(action.permission, obj): + return False + return True + + def checkRoles(self, roles, obj): + return True + + def checkPermission(self, permission, obj): + return True # dummy default states definition @@ -101,6 +140,7 @@ defaultSD = StatesDefinition('default', # states definitions registry +# TODO: use a utility!!! statesDefinitions = dict() diff --git a/stateful/interfaces.py b/stateful/interfaces.py index 9e26b87..50cda62 100644 --- a/stateful/interfaces.py +++ b/stateful/interfaces.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2009 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -33,18 +33,31 @@ 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') - security = Attribute('A callable setting the security settings for ' - 'an object in this state when executed.') + transitions = Attribute('A collection of strings naming the transitions ' + 'that can be executed from this state.') + actions = Attribute('A mapping with actions that may be executed in ' + 'this state.') + setSecurity = Attribute('A callable (argument: stateful object) ' + 'for setting the security settings on the object.') -class ITransition(Interface): +class IAction(Interface): + + name = Attribute('The name or identifier of the action.') + title = Attribute('A user-readable name or title of the action.') + allowed = Attribute('A boolean; if False the action may not be executed.') + doBefore = Attribute('A callable (argument: stateful object) to be executed ' + 'before this action.') + roles = Attribute('A collection of names of the roles that are allowed ' + 'to execute this action; no check when empty.') + permission = Attribute('The name of a permission that is needed for ' + 'executing this action; no check when empty.') + + +class ITransition(IAction): - 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') + 'result of executing this transition.') class IStateful(Interface): @@ -119,9 +132,24 @@ class IStatesDefinition(Interface): """ Return the transitions available for this object in its current state. """ + def isAllowed(action, object): + """ Return True if the action (an IAction provider) is allowed on + the object given for the current user. + """ + + def checkRoles(roles, object): + """ Return True if the current user provides one of the roles given + on the object given. + """ + + def checkRoles(permission, object): + """ Return True if the current user has the permission given + on the object given. + """ + class IStatefulIndexInfo(Interface): - """ Provide a list of tokens to be used for index the states + """ Provide a list of tokens to be used for indexing the states of an object in the catalog. """ @@ -133,7 +161,7 @@ class IStatefulIndexInfo(Interface): class ITransitionEvent(IObjectEvent): - """ Fires when the state of an object is changed. + """ Fires when the state of an object has been changed. """ transition = Attribute('The transition.')