appy.gen: workflows are now implemented as full Appy worlflows. Plone (DC) workflow are not generated anymore. From the Appy user point of view (=developer), no change has occurred: it is a pure implementation concern. This is one more step towards Appy independence from Plone.
This commit is contained in:
parent
93eb16670b
commit
ddec7cd62c
|
@ -1 +1 @@
|
||||||
0.6.7
|
0.7.0
|
||||||
|
|
231
gen/__init__.py
231
gen/__init__.py
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import re, time, copy, sys, types, os, os.path, mimetypes, StringIO
|
import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO
|
||||||
from appy.gen.layout import Table
|
from appy.gen.layout import Table
|
||||||
from appy.gen.layout import defaultFieldLayouts
|
from appy.gen.layout import defaultFieldLayouts
|
||||||
from appy.gen.po import PoMessage
|
from appy.gen.po import PoMessage
|
||||||
|
@ -2167,7 +2167,13 @@ class Pod(Type):
|
||||||
value = value._atFile
|
value = value._atFile
|
||||||
setattr(obj, self.name, value)
|
setattr(obj, self.name, value)
|
||||||
|
|
||||||
# Workflow-specific types ------------------------------------------------------
|
# Workflow-specific types and default workflows --------------------------------
|
||||||
|
appyToZopePermissions = {
|
||||||
|
'read': ('View', 'Access contents information'),
|
||||||
|
'write': 'Modify portal content',
|
||||||
|
'delete': 'Delete objects',
|
||||||
|
}
|
||||||
|
|
||||||
class Role:
|
class Role:
|
||||||
'''Represents a role.'''
|
'''Represents a role.'''
|
||||||
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous',
|
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous',
|
||||||
|
@ -2201,6 +2207,12 @@ class State:
|
||||||
# Standardize the way roles are expressed within self.permissions
|
# Standardize the way roles are expressed within self.permissions
|
||||||
self.standardizeRoles()
|
self.standardizeRoles()
|
||||||
|
|
||||||
|
def getName(self, wf):
|
||||||
|
'''Returns the name for this state in workflow p_wf.'''
|
||||||
|
for name in dir(wf):
|
||||||
|
value = getattr(wf, name)
|
||||||
|
if (value == self): return name
|
||||||
|
|
||||||
def getRole(self, role):
|
def getRole(self, role):
|
||||||
'''p_role can be the name of a role or a Role instance. If it is the
|
'''p_role can be the name of a role or a Role instance. If it is the
|
||||||
name of a role, this method returns self.usedRoles[role] if it
|
name of a role, this method returns self.usedRoles[role] if it
|
||||||
|
@ -2233,16 +2245,6 @@ class State:
|
||||||
|
|
||||||
def getUsedRoles(self): return self.usedRoles.values()
|
def getUsedRoles(self): return self.usedRoles.values()
|
||||||
|
|
||||||
def getTransitions(self, transitions, selfIsFromState=True):
|
|
||||||
'''Among p_transitions, returns those whose fromState is p_self (if
|
|
||||||
p_selfIsFromState is True) or those whose toState is p_self (if
|
|
||||||
p_selfIsFromState is False).'''
|
|
||||||
res = []
|
|
||||||
for t in transitions:
|
|
||||||
if self in t.getStates(selfIsFromState):
|
|
||||||
res.append(t)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getPermissions(self):
|
def getPermissions(self):
|
||||||
'''If you get the permissions mapping through self.permissions, dict
|
'''If you get the permissions mapping through self.permissions, dict
|
||||||
values may be of different types (a list of roles, a single role or
|
values may be of different types (a list of roles, a single role or
|
||||||
|
@ -2258,6 +2260,38 @@ class State:
|
||||||
res[permission] = roleValue
|
res[permission] = roleValue
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def updatePermission(self, obj, zopePermission, roleNames):
|
||||||
|
'''Updates, on p_obj, list of p_roleNames which are granted a given
|
||||||
|
p_zopePermission.'''
|
||||||
|
attr = Permission.getZopeAttrName(zopePermission)
|
||||||
|
if not hasattr(obj.aq_base, attr) or \
|
||||||
|
(getattr(obj.aq_base, attr) != roleNames):
|
||||||
|
setattr(obj, attr, roleNames)
|
||||||
|
|
||||||
|
def updatePermissions(self, wf, obj):
|
||||||
|
'''Zope requires permission-to-roles mappings to be stored as attributes
|
||||||
|
on the object itself. This method does this job, duplicating the info
|
||||||
|
from this state on p_obj.'''
|
||||||
|
for permission, roles in self.getPermissions().iteritems():
|
||||||
|
roleNames = tuple([role.name for role in roles])
|
||||||
|
# Compute Zope permission(s) related to this permission.
|
||||||
|
if appyToZopePermissions.has_key(permission):
|
||||||
|
# It is a standard permission (r, w, d)
|
||||||
|
zopePerm = appyToZopePermissions[permission]
|
||||||
|
elif isinstance(permission, basestring):
|
||||||
|
# It is a user-defined permission
|
||||||
|
zopePerm = permission
|
||||||
|
else:
|
||||||
|
# It is a Permission instance
|
||||||
|
appName = obj.getProductConfig().PROJECTNAME
|
||||||
|
zopePerm = permission.getName(wf, appName)
|
||||||
|
# zopePerm contains a single permission or a tuple of permissions
|
||||||
|
if isinstance(zopePerm, basestring):
|
||||||
|
self.updatePermission(obj, zopePerm, roleNames)
|
||||||
|
else:
|
||||||
|
for zPerm in zopePerm:
|
||||||
|
self.updatePermission(obj, zPerm, roleNames)
|
||||||
|
|
||||||
class Transition:
|
class Transition:
|
||||||
def __init__(self, states, condition=True, action=None, notify=None,
|
def __init__(self, states, condition=True, action=None, notify=None,
|
||||||
show=True, confirm=False):
|
show=True, confirm=False):
|
||||||
|
@ -2277,6 +2311,12 @@ class Transition:
|
||||||
# the transition. It will only be possible by code.
|
# the transition. It will only be possible by code.
|
||||||
self.confirm = confirm # If True, a confirm popup will show up.
|
self.confirm = confirm # If True, a confirm popup will show up.
|
||||||
|
|
||||||
|
def getName(self, wf):
|
||||||
|
'''Returns the name for this state in workflow p_wf.'''
|
||||||
|
for name in dir(wf):
|
||||||
|
value = getattr(wf, name)
|
||||||
|
if (value == self): return name
|
||||||
|
|
||||||
def getUsedRoles(self):
|
def getUsedRoles(self):
|
||||||
'''self.condition can specify a role.'''
|
'''self.condition can specify a role.'''
|
||||||
res = []
|
res = []
|
||||||
|
@ -2296,23 +2336,6 @@ class Transition:
|
||||||
else:
|
else:
|
||||||
return self.show
|
return self.show
|
||||||
|
|
||||||
def getStates(self, fromStates=True):
|
|
||||||
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
|
|
||||||
else. If you want to get the states grouped in tuples
|
|
||||||
(fromState, toState), simply use self.states.'''
|
|
||||||
res = []
|
|
||||||
stateIndex = 1
|
|
||||||
if fromStates:
|
|
||||||
stateIndex = 0
|
|
||||||
if self.isSingle():
|
|
||||||
res.append(self.states[stateIndex])
|
|
||||||
else:
|
|
||||||
for states in self.states:
|
|
||||||
theState = states[stateIndex]
|
|
||||||
if theState not in res:
|
|
||||||
res.append(theState)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def hasState(self, state, isFrom):
|
def hasState(self, state, isFrom):
|
||||||
'''If p_isFrom is True, this method returns True if p_state is a
|
'''If p_isFrom is True, this method returns True if p_state is a
|
||||||
starting state for p_self. If p_isFrom is False, this method returns
|
starting state for p_self. If p_isFrom is False, this method returns
|
||||||
|
@ -2330,6 +2353,117 @@ class Transition:
|
||||||
break
|
break
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def isTriggerable(self, obj, wf):
|
||||||
|
'''Can this transition be triggered on p_obj?'''
|
||||||
|
# Checks that the current state of the object is a start state for this
|
||||||
|
# transition.
|
||||||
|
objState = obj.getState(name=False)
|
||||||
|
if self.isSingle():
|
||||||
|
if objState != self.states[0]: return False
|
||||||
|
else:
|
||||||
|
startFound = False
|
||||||
|
for startState, stopState in self.states:
|
||||||
|
if startState == objState:
|
||||||
|
startFound = True
|
||||||
|
break
|
||||||
|
if not startFound: return False
|
||||||
|
# Check that the condition is met
|
||||||
|
user = obj.portal_membership.getAuthenticatedMember()
|
||||||
|
if isinstance(self.condition, Role):
|
||||||
|
# Condition is a role. Transition may be triggered if the user has
|
||||||
|
# this role.
|
||||||
|
return user.has_role(self.condition.name, obj)
|
||||||
|
elif type(self.condition) == types.FunctionType:
|
||||||
|
return self.condition(wf, obj.appy())
|
||||||
|
elif type(self.condition) in (tuple, list):
|
||||||
|
# It is a list of roles and/or functions. Transition may be
|
||||||
|
# triggered if user has at least one of those roles and if all
|
||||||
|
# functions return True.
|
||||||
|
hasRole = None
|
||||||
|
for roleOrFunction in self.condition:
|
||||||
|
if isinstance(roleOrFunction, basestring):
|
||||||
|
if hasRole == None:
|
||||||
|
hasRole = False
|
||||||
|
if user.has_role(roleOrFunction, obj):
|
||||||
|
hasRole = True
|
||||||
|
elif type(roleOrFunction) == types.FunctionType:
|
||||||
|
if not roleOrFunction(wf, obj.appy()):
|
||||||
|
return False
|
||||||
|
if hasRole != False:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def executeAction(self, obj, wf):
|
||||||
|
'''Executes the action related to this transition.'''
|
||||||
|
msg = ''
|
||||||
|
if type(self.action) in (tuple, list):
|
||||||
|
# We need to execute a list of actions
|
||||||
|
for act in self.action:
|
||||||
|
msgPart = act(wf, obj.appy())
|
||||||
|
if msgPart: msg += msgPart
|
||||||
|
else: # We execute a single action only.
|
||||||
|
msgPart = self.action(wf, obj.appy())
|
||||||
|
if msgPart: msg += msgPart
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def trigger(self, transitionName, obj, wf, comment, doAction=True,
|
||||||
|
doNotify=True, doHistory=True, doSay=True):
|
||||||
|
'''This method triggers this transition on p_obj. The transition is
|
||||||
|
supposed to be triggerable (call to self.isTriggerable must have been
|
||||||
|
performed before calling this method). If p_doAction is False, the
|
||||||
|
action that must normally be executed after the transition has been
|
||||||
|
triggered will not be executed. If p_doNotify is False, the
|
||||||
|
notifications (email,...) that must normally be launched after the
|
||||||
|
transition has been triggered will not be launched. If p_doHistory is
|
||||||
|
False, there will be no trace from this transition triggering in the
|
||||||
|
workflow history. If p_doSay is False, we consider the transition is
|
||||||
|
trigger programmatically, and no message is returned to the user.'''
|
||||||
|
# Create the workflow_history dict if it does not exist.
|
||||||
|
if not hasattr(obj.aq_base, 'workflow_history'):
|
||||||
|
from persistent.mapping import PersistentMapping
|
||||||
|
obj.workflow_history = PersistentMapping()
|
||||||
|
# Create the event list if it does not exist in the dict
|
||||||
|
if not obj.workflow_history: obj.workflow_history['appy'] = ()
|
||||||
|
# Get the key where object history is stored (this overstructure is
|
||||||
|
# only there for backward compatibility reasons)
|
||||||
|
key = obj.workflow_history.keys()[0]
|
||||||
|
# Identify the target state for this transition
|
||||||
|
if self.isSingle():
|
||||||
|
targetState = self.states[1]
|
||||||
|
targetStateName = targetState.getName(wf)
|
||||||
|
else:
|
||||||
|
startState = obj.getState(name=False)
|
||||||
|
for sState, tState in self.states:
|
||||||
|
if startState == sState:
|
||||||
|
targetState = tState
|
||||||
|
targetStateName = targetState.getName(wf)
|
||||||
|
break
|
||||||
|
# Create the event and put it in workflow_history
|
||||||
|
from DateTime import DateTime
|
||||||
|
action = transitionName
|
||||||
|
if transitionName == '_init_': action = None
|
||||||
|
userId = obj.portal_membership.getAuthenticatedMember().getId()
|
||||||
|
if not doHistory: comment = '_invisible_'
|
||||||
|
obj.workflow_history[key] += (
|
||||||
|
{'action':action, 'review_state': targetStateName,
|
||||||
|
'comments': comment, 'actor': userId, 'time': DateTime()},)
|
||||||
|
# Update permissions-to-roles attributes
|
||||||
|
targetState.updatePermissions(wf, obj)
|
||||||
|
# Refresh catalog-related security if required
|
||||||
|
if not obj.isTemporary():
|
||||||
|
obj.reindexObject(idxs=('allowedRolesAndUsers','review_state'))
|
||||||
|
# Execute the related action if needed
|
||||||
|
msg = ''
|
||||||
|
if doAction and self.action: msg = self.executeAction(obj, wf)
|
||||||
|
# Send notifications if needed
|
||||||
|
if doNotify and self.notify and obj.getTool(True).enableNotifications:
|
||||||
|
notifier.sendMail(obj.appy(), self, transitionName, wf)
|
||||||
|
# Return a message to the user if needed
|
||||||
|
if not doSay or (transitionName == '_init_'): return
|
||||||
|
if not msg:
|
||||||
|
msg = obj.translate(u'Your content\'s status has been modified.',
|
||||||
|
domain='plone')
|
||||||
|
obj.say(msg)
|
||||||
|
|
||||||
class Permission:
|
class Permission:
|
||||||
'''If you need to define a specific read or write permission of a given
|
'''If you need to define a specific read or write permission of a given
|
||||||
attribute of an Appy type, you use the specific boolean parameters
|
attribute of an Appy type, you use the specific boolean parameters
|
||||||
|
@ -2346,9 +2480,37 @@ class Permission:
|
||||||
and "specificWritePermission" as booleans. When defining named
|
and "specificWritePermission" as booleans. When defining named
|
||||||
(string) permissions, for referring to it you simply use those strings,
|
(string) permissions, for referring to it you simply use those strings,
|
||||||
you do not create instances of ReadPermission or WritePermission.'''
|
you do not create instances of ReadPermission or WritePermission.'''
|
||||||
|
|
||||||
|
allowedChars = string.digits + string.letters + '_'
|
||||||
|
|
||||||
def __init__(self, fieldDescriptor):
|
def __init__(self, fieldDescriptor):
|
||||||
self.fieldDescriptor = fieldDescriptor
|
self.fieldDescriptor = fieldDescriptor
|
||||||
|
|
||||||
|
def getName(self, wf, appName):
|
||||||
|
'''Returns the name of the Zope permission that corresponds to this
|
||||||
|
permission.'''
|
||||||
|
className, fieldName = self.fieldDescriptor.rsplit('.', 1)
|
||||||
|
if className.find('.') == -1:
|
||||||
|
# The related class resides in the same module as the workflow
|
||||||
|
fullClassName= '%s_%s' % (wf.__module__.replace('.', '_'),className)
|
||||||
|
else:
|
||||||
|
# className contains the full package name of the class
|
||||||
|
fullClassName = className.replace('.', '_')
|
||||||
|
# Read or Write ?
|
||||||
|
if self.__class__.__name__ == 'ReadPermission': access = 'Read'
|
||||||
|
else: access = 'Write'
|
||||||
|
return '%s: %s %s %s' % (appName, access, fullClassName, fieldName)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getZopeAttrName(zopePermission):
|
||||||
|
'''Gets the name of the attribute where Zope stores, on every object,
|
||||||
|
the tuple of roles who are granted a given p_zopePermission.'''
|
||||||
|
res = ''
|
||||||
|
for c in zopePermission:
|
||||||
|
if c in Permission.allowedChars: res += c
|
||||||
|
else: res += '_'
|
||||||
|
return '_%s_Permission' % res
|
||||||
|
|
||||||
class ReadPermission(Permission): pass
|
class ReadPermission(Permission): pass
|
||||||
class WritePermission(Permission): pass
|
class WritePermission(Permission): pass
|
||||||
|
|
||||||
|
@ -2363,6 +2525,17 @@ class No:
|
||||||
def __nonzero__(self):
|
def __nonzero__(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
class WorkflowAnonymous:
|
||||||
|
'''One-state workflow allowing anyone to consult and Manager to edit.'''
|
||||||
|
mgr = 'Manager'
|
||||||
|
active = State({r:[mgr, 'Anonymous'], w:mgr, d:mgr}, initial=True)
|
||||||
|
|
||||||
|
class WorkflowAuthenticated:
|
||||||
|
'''One-state workflow allowing authenticated users to consult and Manager
|
||||||
|
to edit.'''
|
||||||
|
mgr = 'Manager'
|
||||||
|
active = State({r:[mgr, 'Authenticated'], w:mgr, d:mgr}, initial=True)
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class Selection:
|
class Selection:
|
||||||
'''Instances of this class may be given as validator of a String, in order
|
'''Instances of this class may be given as validator of a String, in order
|
||||||
|
|
|
@ -89,141 +89,9 @@ class ClassDescriptor(Descriptor):
|
||||||
|
|
||||||
class WorkflowDescriptor(Descriptor):
|
class WorkflowDescriptor(Descriptor):
|
||||||
'''This class gives information about an Appy workflow.'''
|
'''This class gives information about an Appy workflow.'''
|
||||||
|
@staticmethod
|
||||||
def _getWorkflowElements(self, elemType):
|
def getWorkflowName(klass):
|
||||||
res = []
|
'''Returns the name of this workflow.'''
|
||||||
for attrName in dir(self.klass):
|
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
|
||||||
attrValue = getattr(self.klass, attrName)
|
return res.lower()
|
||||||
condition = False
|
|
||||||
if elemType == 'states':
|
|
||||||
condition = isinstance(attrValue, State)
|
|
||||||
elif elemType == 'transitions':
|
|
||||||
condition = isinstance(attrValue, Transition)
|
|
||||||
elif elemType == 'all':
|
|
||||||
condition = isinstance(attrValue, State) or \
|
|
||||||
isinstance(attrValue, Transition)
|
|
||||||
if condition:
|
|
||||||
res.append(attrValue)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getStates(self):
|
|
||||||
return self._getWorkflowElements('states')
|
|
||||||
|
|
||||||
def getTransitions(self):
|
|
||||||
return self._getWorkflowElements('transitions')
|
|
||||||
|
|
||||||
def getStateNames(self, ordered=False):
|
|
||||||
res = []
|
|
||||||
attrs = dir(self.klass)
|
|
||||||
allAttrs = attrs
|
|
||||||
if ordered:
|
|
||||||
attrs = self.orderedAttributes
|
|
||||||
allAttrs = dir(self.klass)
|
|
||||||
for attrName in attrs:
|
|
||||||
attrValue = getattr(self.klass, attrName)
|
|
||||||
if isinstance(attrValue, State):
|
|
||||||
res.append(attrName)
|
|
||||||
# Complete the list with inherited states. For the moment, we are unable
|
|
||||||
# to sort inherited states.
|
|
||||||
for attrName in allAttrs:
|
|
||||||
attrValue = getattr(self.klass, attrName)
|
|
||||||
if isinstance(attrValue, State) and (attrName not in attrs):
|
|
||||||
res.insert(0, attrName)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getInitialStateName(self):
|
|
||||||
res = None
|
|
||||||
for attrName in dir(self.klass):
|
|
||||||
attrValue = getattr(self.klass, attrName)
|
|
||||||
if isinstance(attrValue, State) and attrValue.initial:
|
|
||||||
res = attrName
|
|
||||||
break
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getTransitionNamesOf(self, transitionName, transition,
|
|
||||||
limitToFromState=None):
|
|
||||||
'''Appy p_transition may correspond to several transitions of the
|
|
||||||
concrete workflow engine used. This method returns in a list the
|
|
||||||
name(s) of the "concrete" transition(s) corresponding to
|
|
||||||
p_transition.'''
|
|
||||||
res = []
|
|
||||||
if transition.isSingle():
|
|
||||||
res.append(transitionName)
|
|
||||||
else:
|
|
||||||
for fromState, toState in transition.states:
|
|
||||||
if not limitToFromState or \
|
|
||||||
(limitToFromState and (fromState == limitToFromState)):
|
|
||||||
fromStateName = self.getNameOf(fromState)
|
|
||||||
toStateName = self.getNameOf(toState)
|
|
||||||
res.append('%s%s%sTo%s%s' % (transitionName,
|
|
||||||
fromStateName[0].upper(), fromStateName[1:],
|
|
||||||
toStateName[0].upper(), toStateName[1:]))
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getTransitionNames(self, limitToTransitions=None, limitToFromState=None,
|
|
||||||
withLabels=False):
|
|
||||||
'''Returns the name of all "concrete" transitions corresponding to the
|
|
||||||
Appy transitions of this worlflow. If p_limitToTransitions is not
|
|
||||||
None, it represents a list of Appy transitions and the result is a
|
|
||||||
list of the names of the "concrete" transitions that correspond to
|
|
||||||
those transitions only. If p_limitToFromState is not None, it
|
|
||||||
represents an Appy state; only transitions having this state as start
|
|
||||||
state will be taken into account. If p_withLabels is True, the method
|
|
||||||
returns a list of tuples (s_transitionName, s_transitionLabel); the
|
|
||||||
label being the name of the Appy transition.'''
|
|
||||||
res = []
|
|
||||||
for attrName in dir(self.klass):
|
|
||||||
attrValue = getattr(self.klass, attrName)
|
|
||||||
if isinstance(attrValue, Transition):
|
|
||||||
# We encountered a transition.
|
|
||||||
t = attrValue
|
|
||||||
tName = attrName
|
|
||||||
if not limitToTransitions or \
|
|
||||||
(limitToTransitions and t in limitToTransitions):
|
|
||||||
# We must take this transition into account according to
|
|
||||||
# param "limitToTransitions".
|
|
||||||
if (not limitToFromState) or \
|
|
||||||
(limitToFromState and \
|
|
||||||
t.hasState(limitToFromState, isFrom=True)):
|
|
||||||
# We must take this transition into account according
|
|
||||||
# to param "limitToFromState"
|
|
||||||
tNames = self.getTransitionNamesOf(
|
|
||||||
tName, t, limitToFromState)
|
|
||||||
if not withLabels:
|
|
||||||
res += tNames
|
|
||||||
else:
|
|
||||||
for tn in tNames:
|
|
||||||
res.append((tn, tName))
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getEndStateName(self, transitionName):
|
|
||||||
'''Returns the name of the state where the "concrete" transition named
|
|
||||||
p_transitionName ends.'''
|
|
||||||
res = None
|
|
||||||
for attrName in dir(self.klass):
|
|
||||||
attrValue = getattr(self.klass, attrName)
|
|
||||||
if isinstance(attrValue, Transition):
|
|
||||||
# We got a transition.
|
|
||||||
t = attrValue
|
|
||||||
tName = attrName
|
|
||||||
if t.isSingle():
|
|
||||||
if transitionName == tName:
|
|
||||||
endState = t.states[1]
|
|
||||||
res = self.getNameOf(endState)
|
|
||||||
else:
|
|
||||||
transNames = self.getTransitionNamesOf(tName, t)
|
|
||||||
if transitionName in transNames:
|
|
||||||
endState = t.states[transNames.index(transitionName)][1]
|
|
||||||
res = self.getNameOf(endState)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getNameOf(self, stateOrTransition):
|
|
||||||
'''Gets the Appy name of a p_stateOrTransition.'''
|
|
||||||
res = None
|
|
||||||
for attrName in dir(self.klass):
|
|
||||||
attrValue = getattr(self.klass, attrName)
|
|
||||||
if attrValue == stateOrTransition:
|
|
||||||
res = attrName
|
|
||||||
break
|
|
||||||
return res
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -598,122 +598,4 @@ class TranslationClassDescriptor(ClassDescriptor):
|
||||||
params['format'] = String.TEXT
|
params['format'] = String.TEXT
|
||||||
params['height'] = height
|
params['height'] = height
|
||||||
self.addField(messageId, String(**params))
|
self.addField(messageId, String(**params))
|
||||||
|
|
||||||
class WorkflowDescriptor(appy.gen.descriptors.WorkflowDescriptor):
|
|
||||||
'''Represents a workflow.'''
|
|
||||||
# How to map Appy permissions to Plone permissions ?
|
|
||||||
appyToPlonePermissions = {
|
|
||||||
'read': ('View', 'Access contents information'),
|
|
||||||
'write': ('Modify portal content',),
|
|
||||||
'delete': ('Delete objects',),
|
|
||||||
}
|
|
||||||
def getPlonePermissions(self, permission):
|
|
||||||
'''Returns the Plone permission(s) that correspond to
|
|
||||||
Appy p_permission.'''
|
|
||||||
if self.appyToPlonePermissions.has_key(permission):
|
|
||||||
res = self.appyToPlonePermissions[permission]
|
|
||||||
elif isinstance(permission, basestring):
|
|
||||||
res = [permission]
|
|
||||||
else:
|
|
||||||
# Permission if an Appy permission declaration
|
|
||||||
className, fieldName = permission.fieldDescriptor.rsplit('.', 1)
|
|
||||||
if className.find('.') == -1:
|
|
||||||
# The related class resides in the same module as the workflow
|
|
||||||
fullClassName = '%s_%s' % (
|
|
||||||
self.klass.__module__.replace('.', '_'), className)
|
|
||||||
else:
|
|
||||||
# className contains the full package name of the class
|
|
||||||
fullClassName = className.replace('.', '_')
|
|
||||||
# Read or Write ?
|
|
||||||
if permission.__class__.__name__ == 'ReadPermission':
|
|
||||||
access = 'Read'
|
|
||||||
else:
|
|
||||||
access = 'Write'
|
|
||||||
permName = '%s: %s %s %s' % (self.generator.applicationName,
|
|
||||||
access, fullClassName, fieldName)
|
|
||||||
res = [permName]
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getWorkflowName(klass):
|
|
||||||
'''Generates the name of the corresponding Archetypes workflow.'''
|
|
||||||
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
|
|
||||||
return res.lower()
|
|
||||||
getWorkflowName = staticmethod(getWorkflowName)
|
|
||||||
|
|
||||||
def getStatesInfo(self, asDumpableCode=False):
|
|
||||||
'''Gets, in a dict, information for configuring states of the workflow.
|
|
||||||
If p_asDumpableCode is True, instead of returning a dict, this
|
|
||||||
method will return a string containing the dict that can be dumped
|
|
||||||
into a Python code file.'''
|
|
||||||
res = {}
|
|
||||||
transitions = self.getTransitions()
|
|
||||||
for state in self.getStates():
|
|
||||||
stateName = self.getNameOf(state)
|
|
||||||
# We need the list of transitions that start from this state
|
|
||||||
outTransitions = state.getTransitions(transitions,
|
|
||||||
selfIsFromState=True)
|
|
||||||
tNames = self.getTransitionNames(outTransitions,
|
|
||||||
limitToFromState=state)
|
|
||||||
# Compute the permissions/roles mapping for this state
|
|
||||||
permissionsMapping = {}
|
|
||||||
for permission, roles in state.getPermissions().iteritems():
|
|
||||||
for plonePerm in self.getPlonePermissions(permission):
|
|
||||||
permissionsMapping[plonePerm] = [r.name for r in roles]
|
|
||||||
# Add 'Review portal content' to anyone; this is not a security
|
|
||||||
# problem because we limit the triggering of every transition
|
|
||||||
# individually.
|
|
||||||
allRoles = [r.name for r in self.generator.getAllUsedRoles()]
|
|
||||||
if 'Manager' not in allRoles: allRoles.append('Manager')
|
|
||||||
permissionsMapping['Review portal content'] = allRoles
|
|
||||||
res[stateName] = (tNames, permissionsMapping)
|
|
||||||
if not asDumpableCode:
|
|
||||||
return res
|
|
||||||
# We must create the "Python code" version of this dict
|
|
||||||
newRes = '{'
|
|
||||||
for stateName, stateInfo in res.iteritems():
|
|
||||||
transitions = ','.join(['"%s"' % tn for tn in stateInfo[0]])
|
|
||||||
# Compute permissions
|
|
||||||
permissions = ''
|
|
||||||
for perm, roles in stateInfo[1].iteritems():
|
|
||||||
theRoles = ','.join(['"%s"' % r for r in roles])
|
|
||||||
permissions += '"%s": [%s],' % (perm, theRoles)
|
|
||||||
newRes += '\n "%s": ([%s], {%s}),' % \
|
|
||||||
(stateName, transitions, permissions)
|
|
||||||
return newRes + '}'
|
|
||||||
|
|
||||||
def getTransitionsInfo(self, asDumpableCode=False):
|
|
||||||
'''Gets, in a dict, information for configuring transitions of the
|
|
||||||
workflow. If p_asDumpableCode is True, instead of returning a dict,
|
|
||||||
this method will return a string containing the dict that can be
|
|
||||||
dumped into a Python code file.'''
|
|
||||||
res = {}
|
|
||||||
for tName in self.getTransitionNames():
|
|
||||||
res[tName] = self.getEndStateName(tName)
|
|
||||||
if not asDumpableCode:
|
|
||||||
return res
|
|
||||||
# We must create the "Python code" version of this dict
|
|
||||||
newRes = '{'
|
|
||||||
for transitionName, endStateName in res.iteritems():
|
|
||||||
newRes += '\n "%s": "%s",' % (transitionName, endStateName)
|
|
||||||
return newRes + '}'
|
|
||||||
|
|
||||||
def getManagedPermissions(self):
|
|
||||||
'''Returns the Plone permissions of all Appy permissions managed by this
|
|
||||||
workflow.'''
|
|
||||||
res = set()
|
|
||||||
res.add('Review portal content')
|
|
||||||
for state in self.getStates():
|
|
||||||
for permission in state.permissions.iterkeys():
|
|
||||||
for plonePerm in self.getPlonePermissions(permission):
|
|
||||||
res.add(plonePerm)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getScripts(self):
|
|
||||||
res = ''
|
|
||||||
wfName = WorkflowDescriptor.getWorkflowName(self.klass)
|
|
||||||
for tName in self.getTransitionNames():
|
|
||||||
scriptName = '%s_do%s%s' % (wfName, tName[0].upper(), tName[1:])
|
|
||||||
res += 'def %s(self, stateChange, **kw): do("%s", ' \
|
|
||||||
'stateChange, logger)\n' % (scriptName, tName)
|
|
||||||
return res
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -8,9 +8,9 @@ from appy.gen import *
|
||||||
from appy.gen.po import PoMessage, PoFile, PoParser
|
from appy.gen.po import PoMessage, PoFile, PoParser
|
||||||
from appy.gen.generator import Generator as AbstractGenerator
|
from appy.gen.generator import Generator as AbstractGenerator
|
||||||
from appy.gen.utils import getClassName
|
from appy.gen.utils import getClassName
|
||||||
from descriptors import ClassDescriptor, WorkflowDescriptor, \
|
from appy.gen.descriptors import WorkflowDescriptor
|
||||||
ToolClassDescriptor, UserClassDescriptor, \
|
from descriptors import ClassDescriptor, ToolClassDescriptor, \
|
||||||
TranslationClassDescriptor
|
UserClassDescriptor, TranslationClassDescriptor
|
||||||
from model import ModelClass, User, Tool, Translation
|
from model import ModelClass, User, Tool, Translation
|
||||||
|
|
||||||
# Common methods that need to be defined on every Archetype class --------------
|
# Common methods that need to be defined on every Archetype class --------------
|
||||||
|
@ -38,7 +38,6 @@ class Generator(AbstractGenerator):
|
||||||
AbstractGenerator.__init__(self, *args, **kwargs)
|
AbstractGenerator.__init__(self, *args, **kwargs)
|
||||||
# Set our own Descriptor classes
|
# Set our own Descriptor classes
|
||||||
self.descriptorClasses['class'] = ClassDescriptor
|
self.descriptorClasses['class'] = ClassDescriptor
|
||||||
self.descriptorClasses['workflow'] = WorkflowDescriptor
|
|
||||||
# Create our own Tool, User and Translation instances
|
# Create our own Tool, User and Translation instances
|
||||||
self.tool = ToolClassDescriptor(Tool, self)
|
self.tool = ToolClassDescriptor(Tool, self)
|
||||||
self.user = UserClassDescriptor(User, self)
|
self.user = UserClassDescriptor(User, self)
|
||||||
|
@ -159,7 +158,6 @@ class Generator(AbstractGenerator):
|
||||||
# Create basic files (config.py, Install.py, etc)
|
# Create basic files (config.py, Install.py, etc)
|
||||||
self.generateTool()
|
self.generateTool()
|
||||||
self.generateInit()
|
self.generateInit()
|
||||||
self.generateWorkflows()
|
|
||||||
self.generateTests()
|
self.generateTests()
|
||||||
if self.config.frontPage:
|
if self.config.frontPage:
|
||||||
self.generateFrontPage()
|
self.generateFrontPage()
|
||||||
|
@ -368,33 +366,6 @@ class Generator(AbstractGenerator):
|
||||||
catalogMap += "catalogMap['%s']['black'] = " \
|
catalogMap += "catalogMap['%s']['black'] = " \
|
||||||
"['portal_catalog']\n" % blackClass
|
"['portal_catalog']\n" % blackClass
|
||||||
repls['catalogMap'] = catalogMap
|
repls['catalogMap'] = catalogMap
|
||||||
# Compute workflows
|
|
||||||
workflows = ''
|
|
||||||
for classDescr in classesAll:
|
|
||||||
if hasattr(classDescr.klass, 'workflow'):
|
|
||||||
wfName = WorkflowDescriptor.getWorkflowName(
|
|
||||||
classDescr.klass.workflow)
|
|
||||||
workflows += '\n "%s":"%s",' % (classDescr.name, wfName)
|
|
||||||
repls['workflows'] = workflows
|
|
||||||
# Compute workflow instances initialisation
|
|
||||||
wfInit = ''
|
|
||||||
for workflowDescr in self.workflows:
|
|
||||||
k = workflowDescr.klass
|
|
||||||
className = '%s.%s' % (k.__module__, k.__name__)
|
|
||||||
wfInit += 'wf = %s()\n' % className
|
|
||||||
wfInit += 'wf._transitionsMapping = {}\n'
|
|
||||||
for transition in workflowDescr.getTransitions():
|
|
||||||
tName = workflowDescr.getNameOf(transition)
|
|
||||||
tNames = workflowDescr.getTransitionNamesOf(tName, transition)
|
|
||||||
for trName in tNames:
|
|
||||||
wfInit += 'wf._transitionsMapping["%s"] = wf.%s\n' % \
|
|
||||||
(trName, tName)
|
|
||||||
# We need a new attribute that stores states in order
|
|
||||||
wfInit += 'wf._states = []\n'
|
|
||||||
for stateName in workflowDescr.getStateNames(ordered=True):
|
|
||||||
wfInit += 'wf._states.append("%s")\n' % stateName
|
|
||||||
wfInit += 'workflowInstances[%s] = wf\n' % className
|
|
||||||
repls['workflowInstancesInit'] = wfInit
|
|
||||||
# Compute the list of ordered attributes (forward and backward,
|
# Compute the list of ordered attributes (forward and backward,
|
||||||
# inherited included) for every Appy class.
|
# inherited included) for every Appy class.
|
||||||
attributes = []
|
attributes = []
|
||||||
|
@ -463,40 +434,6 @@ class Generator(AbstractGenerator):
|
||||||
repls['totalNumberOfTests'] = self.totalNumberOfTests
|
repls['totalNumberOfTests'] = self.totalNumberOfTests
|
||||||
self.copyFile('__init__.py', repls)
|
self.copyFile('__init__.py', repls)
|
||||||
|
|
||||||
def generateWorkflows(self):
|
|
||||||
'''Generates the file that contains one function by workflow.
|
|
||||||
Those functions are called by Plone for registering the workflows.'''
|
|
||||||
workflows = ''
|
|
||||||
for wfDescr in self.workflows:
|
|
||||||
# Compute state names & info, transition names & infos, managed
|
|
||||||
# permissions
|
|
||||||
stateNames=','.join(['"%s"' % sn for sn in wfDescr.getStateNames()])
|
|
||||||
stateInfos = wfDescr.getStatesInfo(asDumpableCode=True)
|
|
||||||
transitionNames = ','.join(['"%s"' % tn for tn in \
|
|
||||||
wfDescr.getTransitionNames()])
|
|
||||||
transitionInfos = wfDescr.getTransitionsInfo(asDumpableCode=True)
|
|
||||||
managedPermissions = ','.join(['"%s"' % tn for tn in \
|
|
||||||
wfDescr.getManagedPermissions()])
|
|
||||||
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
|
||||||
workflows += '%s\ndef create_%s(self, id):\n ' \
|
|
||||||
'stateNames = [%s]\n ' \
|
|
||||||
'stateInfos = %s\n ' \
|
|
||||||
'transitionNames = [%s]\n ' \
|
|
||||||
'transitionInfos = %s\n ' \
|
|
||||||
'managedPermissions = [%s]\n ' \
|
|
||||||
'return WorkflowCreator("%s", DCWorkflowDefinition, ' \
|
|
||||||
'stateNames, "%s", stateInfos, transitionNames, ' \
|
|
||||||
'transitionInfos, managedPermissions, PROJECTNAME, ' \
|
|
||||||
'ExternalMethod).run()\n' \
|
|
||||||
'addWorkflowFactory(create_%s,\n id="%s",\n ' \
|
|
||||||
'title="%s")\n\n' % (wfDescr.getScripts(), wfName, stateNames,
|
|
||||||
stateInfos, transitionNames, transitionInfos,
|
|
||||||
managedPermissions, wfName, wfDescr.getInitialStateName(),
|
|
||||||
wfName, wfName, wfName)
|
|
||||||
repls = self.repls.copy()
|
|
||||||
repls['workflows'] = workflows
|
|
||||||
self.copyFile('workflows.py', repls, destFolder='Extensions')
|
|
||||||
|
|
||||||
def generateWrapperProperty(self, name, type):
|
def generateWrapperProperty(self, name, type):
|
||||||
'''Generates the getter for attribute p_name.'''
|
'''Generates the getter for attribute p_name.'''
|
||||||
res = ' def get_%s(self):\n ' % name
|
res = ' def get_%s(self):\n ' % name
|
||||||
|
@ -519,7 +456,6 @@ class Generator(AbstractGenerator):
|
||||||
* "custom" it includes descriptors for the config-related classes
|
* "custom" it includes descriptors for the config-related classes
|
||||||
for which the user has created a sub-class.'''
|
for which the user has created a sub-class.'''
|
||||||
if not include: return self.classes
|
if not include: return self.classes
|
||||||
else:
|
|
||||||
res = self.classes[:]
|
res = self.classes[:]
|
||||||
configClasses = [self.tool, self.user, self.translation]
|
configClasses = [self.tool, self.user, self.translation]
|
||||||
if include == 'all':
|
if include == 'all':
|
||||||
|
@ -793,44 +729,39 @@ class Generator(AbstractGenerator):
|
||||||
self.copyFile('Class.py', repls, destName=fileName)
|
self.copyFile('Class.py', repls, destName=fileName)
|
||||||
|
|
||||||
def generateWorkflow(self, wfDescr):
|
def generateWorkflow(self, wfDescr):
|
||||||
'''This method does not generate the workflow definition, which is done
|
'''This method creates the i18n labels related to the workflow described
|
||||||
in self.generateWorkflows. This method just creates the i18n labels
|
by p_wfDescr.'''
|
||||||
related to the workflow described by p_wfDescr.'''
|
|
||||||
k = wfDescr.klass
|
k = wfDescr.klass
|
||||||
print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__)
|
print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__)
|
||||||
# Identify Plone workflow name
|
# Identify workflow name
|
||||||
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
||||||
# Add i18n messages for states and transitions
|
# Add i18n messages for states
|
||||||
for sName in wfDescr.getStateNames():
|
for name in dir(wfDescr.klass):
|
||||||
poMsg = PoMessage('%s_%s' % (wfName, sName), '', sName)
|
if not isinstance(getattr(wfDescr.klass, name), State): continue
|
||||||
|
poMsg = PoMessage('%s_%s' % (wfName, name), '', name)
|
||||||
poMsg.produceNiceDefault()
|
poMsg.produceNiceDefault()
|
||||||
self.labels.append(poMsg)
|
self.labels.append(poMsg)
|
||||||
for tName, tLabel in wfDescr.getTransitionNames(withLabels=True):
|
# Add i18n messages for transitions
|
||||||
poMsg = PoMessage('%s_%s' % (wfName, tName), '', tLabel)
|
for name in dir(wfDescr.klass):
|
||||||
|
transition = getattr(wfDescr.klass, name)
|
||||||
|
if not isinstance(transition, Transition): continue
|
||||||
|
poMsg = PoMessage('%s_%s' % (wfName, name), '', name)
|
||||||
poMsg.produceNiceDefault()
|
poMsg.produceNiceDefault()
|
||||||
self.labels.append(poMsg)
|
self.labels.append(poMsg)
|
||||||
for transition in wfDescr.getTransitions():
|
|
||||||
# Get the Appy transition name
|
|
||||||
tName = wfDescr.getNameOf(transition)
|
|
||||||
# Get the names of the corresponding DC transition(s)
|
|
||||||
tNames = wfDescr.getTransitionNamesOf(tName, transition)
|
|
||||||
if transition.confirm:
|
if transition.confirm:
|
||||||
# We need to generate a label for the message that will be shown
|
# We need to generate a label for the message that will be shown
|
||||||
# in the confirm popup.
|
# in the confirm popup.
|
||||||
for tn in tNames:
|
label = '%s_%s_confirm' % (wfName, name)
|
||||||
label = '%s_%s_confirm' % (wfName, tn)
|
|
||||||
poMsg = PoMessage(label, '', PoMessage.CONFIRM)
|
poMsg = PoMessage(label, '', PoMessage.CONFIRM)
|
||||||
self.labels.append(poMsg)
|
self.labels.append(poMsg)
|
||||||
if transition.notify:
|
if transition.notify:
|
||||||
# Appy will send a mail when this transition is triggered.
|
# Appy will send a mail when this transition is triggered.
|
||||||
# So we need 2 i18n labels for every DC transition corresponding
|
# So we need 2 i18n labels: one for the mail subject and one for
|
||||||
# to this Appy transition: one for the mail subject and one for
|
|
||||||
# the mail body.
|
# the mail body.
|
||||||
for tn in tNames:
|
subjectLabel = '%s_%s_mail_subject' % (wfName, name)
|
||||||
subjectLabel = '%s_%s_mail_subject' % (wfName, tn)
|
|
||||||
poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT)
|
poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT)
|
||||||
self.labels.append(poMsg)
|
self.labels.append(poMsg)
|
||||||
bodyLabel = '%s_%s_mail_body' % (wfName, tn)
|
bodyLabel = '%s_%s_mail_body' % (wfName, name)
|
||||||
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
|
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
|
||||||
self.labels.append(poMsg)
|
self.labels.append(poMsg)
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -34,7 +34,6 @@ class PloneInstaller:
|
||||||
self.catalogMap = cfg.catalogMap
|
self.catalogMap = cfg.catalogMap
|
||||||
self.applicationRoles = cfg.applicationRoles # Roles defined in the app
|
self.applicationRoles = cfg.applicationRoles # Roles defined in the app
|
||||||
self.defaultAddRoles = cfg.defaultAddRoles
|
self.defaultAddRoles = cfg.defaultAddRoles
|
||||||
self.workflows = cfg.workflows
|
|
||||||
self.appFrontPage = cfg.appFrontPage
|
self.appFrontPage = cfg.appFrontPage
|
||||||
self.showPortlet = cfg.showPortlet
|
self.showPortlet = cfg.showPortlet
|
||||||
self.languages = cfg.languages
|
self.languages = cfg.languages
|
||||||
|
@ -378,22 +377,6 @@ class PloneInstaller:
|
||||||
site.portal_groups.setRolesForGroup(group, [role])
|
site.portal_groups.setRolesForGroup(group, [role])
|
||||||
site.__ac_roles__ = tuple(data)
|
site.__ac_roles__ = tuple(data)
|
||||||
|
|
||||||
def installWorkflows(self):
|
|
||||||
'''Creates or updates the workflows defined in the application.'''
|
|
||||||
wfTool = self.ploneSite.portal_workflow
|
|
||||||
for contentType, workflowName in self.workflows.iteritems():
|
|
||||||
# Register the workflow if needed
|
|
||||||
if workflowName not in wfTool.listWorkflows():
|
|
||||||
wfMethod = self.config.ExternalMethod('temp', 'temp',
|
|
||||||
self.productName + '.workflows', 'create_%s' % workflowName)
|
|
||||||
workflow = wfMethod(self, workflowName)
|
|
||||||
wfTool._setObject(workflowName, workflow)
|
|
||||||
else:
|
|
||||||
self.appyTool.log('%s already in workflows.' % workflowName)
|
|
||||||
# Link the workflow to the current content type
|
|
||||||
wfTool.setChainForPortalTypes([contentType], workflowName)
|
|
||||||
return wfTool
|
|
||||||
|
|
||||||
def installStyleSheet(self):
|
def installStyleSheet(self):
|
||||||
'''Registers In Plone the stylesheet linked to this application.'''
|
'''Registers In Plone the stylesheet linked to this application.'''
|
||||||
cssName = self.productName + '.css'
|
cssName = self.productName + '.css'
|
||||||
|
@ -495,7 +478,6 @@ class PloneInstaller:
|
||||||
self.installTool()
|
self.installTool()
|
||||||
self.installTranslations()
|
self.installTranslations()
|
||||||
self.installRolesAndGroups()
|
self.installRolesAndGroups()
|
||||||
self.installWorkflows()
|
|
||||||
self.installStyleSheet()
|
self.installStyleSheet()
|
||||||
self.managePortlets()
|
self.managePortlets()
|
||||||
self.manageIndexes()
|
self.manageIndexes()
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import os, os.path, sys, types, mimetypes, urllib, cgi
|
import os, os.path, sys, types, mimetypes, urllib, cgi
|
||||||
import appy.gen
|
import appy.gen
|
||||||
from appy.gen import Type, String, Selection, Role, No
|
from appy.gen import Type, String, Selection, Role, No, WorkflowAnonymous, \
|
||||||
|
Transition
|
||||||
from appy.gen.utils import *
|
from appy.gen.utils import *
|
||||||
from appy.gen.layout import Table, defaultPageLayouts
|
from appy.gen.layout import Table, defaultPageLayouts
|
||||||
|
from appy.gen.descriptors import WorkflowDescriptor
|
||||||
from appy.gen.plone25.descriptors import ClassDescriptor
|
from appy.gen.plone25.descriptors import ClassDescriptor
|
||||||
from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard
|
from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard
|
||||||
|
|
||||||
|
@ -300,6 +302,27 @@ class BaseMixin:
|
||||||
else: logMethod = logger.info
|
else: logMethod = logger.info
|
||||||
logMethod(msg)
|
logMethod(msg)
|
||||||
|
|
||||||
|
def getState(self, name=True):
|
||||||
|
'''Returns state information about this object. If p_name is True, the
|
||||||
|
returned info is the state name. Else, it is the State instance.'''
|
||||||
|
if hasattr(self.aq_base, 'workflow_history'):
|
||||||
|
key = self.workflow_history.keys()[0]
|
||||||
|
stateName = self.workflow_history[key][-1]['review_state']
|
||||||
|
if name: return stateName
|
||||||
|
else: return getattr(self.getWorkflow(), stateName)
|
||||||
|
else:
|
||||||
|
# No workflow information is available (yet) on this object. So
|
||||||
|
# return the workflow initial state.
|
||||||
|
wf = self.getWorkflow()
|
||||||
|
initStateName = 'active'
|
||||||
|
for elem in dir(wf):
|
||||||
|
attr = getattr(wf, elem)
|
||||||
|
if (attr.__class__.__name__ == 'State') and attr.initial:
|
||||||
|
initStateName = elem
|
||||||
|
break
|
||||||
|
if name: return initStateName
|
||||||
|
else: return getattr(wf, initStateName)
|
||||||
|
|
||||||
def rememberPreviousData(self):
|
def rememberPreviousData(self):
|
||||||
'''This method is called before updating an object and remembers, for
|
'''This method is called before updating an object and remembers, for
|
||||||
every historized field, the previous value. Result is a dict
|
every historized field, the previous value. Result is a dict
|
||||||
|
@ -327,11 +350,10 @@ class BaseMixin:
|
||||||
else:
|
else:
|
||||||
changes[fieldName] = (changes[fieldName], appyType.labelId)
|
changes[fieldName] = (changes[fieldName], appyType.labelId)
|
||||||
# Create the event to record in the history
|
# Create the event to record in the history
|
||||||
DateTime = self.getProductConfig().DateTime
|
from DateTime import DateTime
|
||||||
state = self.portal_workflow.getInfoFor(self, 'review_state')
|
|
||||||
user = self.portal_membership.getAuthenticatedMember()
|
user = self.portal_membership.getAuthenticatedMember()
|
||||||
event = {'action': '_datachange_', 'changes': changes,
|
event = {'action': '_datachange_', 'changes': changes,
|
||||||
'review_state': state, 'actor': user.id,
|
'review_state': self.getState(), 'actor': user.id,
|
||||||
'time': DateTime(), 'comments': ''}
|
'time': DateTime(), 'comments': ''}
|
||||||
# Add the event to the history
|
# Add the event to the history
|
||||||
histKey = self.workflow_history.keys()[0]
|
histKey = self.workflow_history.keys()[0]
|
||||||
|
@ -583,16 +605,15 @@ class BaseMixin:
|
||||||
'''Returns information about the states that are related to p_phase.
|
'''Returns information about the states that are related to p_phase.
|
||||||
If p_currentOnly is True, we return the current state, even if not
|
If p_currentOnly is True, we return the current state, even if not
|
||||||
related to p_phase.'''
|
related to p_phase.'''
|
||||||
res = []
|
currentState = self.getState()
|
||||||
dcWorkflow = self.getWorkflow(appy=False)
|
|
||||||
if not dcWorkflow: return res
|
|
||||||
currentState = self.portal_workflow.getInfoFor(self, 'review_state')
|
|
||||||
if currentOnly:
|
if currentOnly:
|
||||||
return [StateDescr(currentState,'current').get()]
|
return [StateDescr(currentState, 'current').get()]
|
||||||
workflow = self.getWorkflow(appy=True)
|
res = []
|
||||||
if workflow:
|
workflow = self.getWorkflow()
|
||||||
stateStatus = 'done'
|
stateStatus = 'done'
|
||||||
for stateName in workflow._states:
|
for stateName in dir(workflow):
|
||||||
|
if getattr(workflow, stateName).__class__.__name__ != 'State':
|
||||||
|
continue
|
||||||
if stateName == currentState:
|
if stateName == currentState:
|
||||||
stateStatus = 'current'
|
stateStatus = 'current'
|
||||||
elif stateStatus != 'done':
|
elif stateStatus != 'done':
|
||||||
|
@ -604,41 +625,28 @@ class BaseMixin:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
||||||
'''This method is similar to portal_workflow.getTransitionsFor, but:
|
'''This method returns info about transitions that one can trigger from
|
||||||
* is able (or not, depending on boolean p_includeFake) to retrieve
|
the user interface.
|
||||||
transitions that the user can't trigger, but for which he needs to
|
* if p_includeFake is True, it retrieves transitions that the user
|
||||||
know for what reason he can't trigger it;
|
can't trigger, but for which he needs to know for what reason he
|
||||||
* is able (or not, depending on p_includeNotShowable) to include
|
can't trigger it;
|
||||||
transitions for which show=False at the Appy level. Indeed, because
|
* if p_includeNotShowable is True, it includes transitions for which
|
||||||
"showability" is only a GUI concern, and not a security concern,
|
show=False. Indeed, because "showability" is only a GUI concern,
|
||||||
in some cases it has sense to set includeNotShowable=True, because
|
and not a security concern, in some cases it has sense to set
|
||||||
those transitions are triggerable from a security point of view;
|
includeNotShowable=True, because those transitions are triggerable
|
||||||
* the transition-info is richer: it contains fake-related info (as
|
from a security point of view.
|
||||||
described above) and confirm-related info (ie, when clicking on
|
'''
|
||||||
the button, do we ask the user to confirm via a popup?)'''
|
|
||||||
res = []
|
res = []
|
||||||
# Get some Plone stuff from the Plone-level config.py
|
wf = self.getWorkflow()
|
||||||
TRIGGER_USER_ACTION = self.getProductConfig().TRIGGER_USER_ACTION
|
currentState = self.getState(name=False)
|
||||||
sm = self.getProductConfig().getSecurityManager
|
# Loop on every transition
|
||||||
# Get the workflow definition for p_obj.
|
for name in dir(wf):
|
||||||
workflow = self.getWorkflow(appy=False)
|
transition = getattr(wf, name)
|
||||||
if not workflow: return res
|
if (transition.__class__.__name__ != 'Transition'): continue
|
||||||
appyWorkflow = self.getWorkflow(appy=True)
|
# Filter transitions that do not have currentState as start state
|
||||||
# What is the current state for this object?
|
if not transition.hasState(currentState, True): continue
|
||||||
currentState = workflow._getWorkflowStateOf(self)
|
# Check if the transition can be triggered
|
||||||
if not currentState: return res
|
mayTrigger = transition.isTriggerable(self, wf)
|
||||||
# Analyse all the transitions that start from this state.
|
|
||||||
for transitionId in currentState.transitions:
|
|
||||||
transition = workflow.transitions.get(transitionId, None)
|
|
||||||
appyTr = appyWorkflow._transitionsMapping[transitionId]
|
|
||||||
if not transition or (transition.trigger_type!=TRIGGER_USER_ACTION)\
|
|
||||||
or not transition.actbox_name: continue
|
|
||||||
# We have a possible candidate for a user-triggerable transition
|
|
||||||
if transition.guard is None:
|
|
||||||
mayTrigger = True
|
|
||||||
else:
|
|
||||||
mayTrigger = checkTransitionGuard(transition.guard, sm(),
|
|
||||||
workflow, self)
|
|
||||||
# Compute the condition that will lead to including or not this
|
# Compute the condition that will lead to including or not this
|
||||||
# transition
|
# transition
|
||||||
if not includeFake:
|
if not includeFake:
|
||||||
|
@ -646,19 +654,15 @@ class BaseMixin:
|
||||||
else:
|
else:
|
||||||
includeIt = mayTrigger or isinstance(mayTrigger, No)
|
includeIt = mayTrigger or isinstance(mayTrigger, No)
|
||||||
if not includeNotShowable:
|
if not includeNotShowable:
|
||||||
includeIt = includeIt and appyTr.isShowable(appyWorkflow, self)
|
includeIt = includeIt and transition.isShowable(wf, self)
|
||||||
if not includeIt: continue
|
if not includeIt: continue
|
||||||
# Add transition-info to the result.
|
# Add transition-info to the result.
|
||||||
tInfo = {'id': transition.id, 'title': transition.title,
|
label = self.getWorkflowLabel(name)
|
||||||
'title_or_id': transition.title_or_id(),
|
tInfo = {'name': name, 'title': self.translate(label),
|
||||||
'description': transition.description, 'confirm': '',
|
'confirm': '', 'may_trigger': True}
|
||||||
'name': transition.actbox_name, 'may_trigger': True,
|
if transition.confirm:
|
||||||
'url': transition.actbox_url %
|
cLabel = '%s_confirm' % label
|
||||||
{'content_url': self.absolute_url(),
|
tInfo['confirm'] = self.translate(cLabel, format='js')
|
||||||
'portal_url' : '', 'folder_url' : ''}}
|
|
||||||
if appyTr.confirm:
|
|
||||||
label = '%s_confirm' % tInfo['name']
|
|
||||||
tInfo['confirm'] = self.translate(label, format='js')
|
|
||||||
if not mayTrigger:
|
if not mayTrigger:
|
||||||
tInfo['may_trigger'] = False
|
tInfo['may_trigger'] = False
|
||||||
tInfo['reason'] = mayTrigger.msg
|
tInfo['reason'] = mayTrigger.msg
|
||||||
|
@ -793,39 +797,32 @@ class BaseMixin:
|
||||||
reverse = rq.get('reverse') == 'True'
|
reverse = rq.get('reverse') == 'True'
|
||||||
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
|
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
|
||||||
|
|
||||||
def getWorkflow(self, appy=True):
|
def notifyWorkflowCreated(self):
|
||||||
'''Returns the Appy workflow instance that is relevant for this
|
'''This method is called by Zope/CMF every time an object is created,
|
||||||
object. If p_appy is False, it returns the DC workflow.'''
|
be it temp or not. The objective here is to initialise workflow-
|
||||||
res = None
|
related data on the object.'''
|
||||||
if appy:
|
wf = self.getWorkflow()
|
||||||
# Get the workflow class first
|
# Get the initial workflow state
|
||||||
workflowClass = None
|
initialState = self.getState(name=False)
|
||||||
if self.wrapperClass:
|
# Create a Transition instance representing the initial transition.
|
||||||
|
initialTransition = Transition((initialState, initialState))
|
||||||
|
initialTransition.trigger('_init_', self, wf, '')
|
||||||
|
|
||||||
|
def getWorkflow(self, name=False):
|
||||||
|
'''Returns the workflow applicable for p_self (or its name, if p_name
|
||||||
|
is True).'''
|
||||||
appyClass = self.wrapperClass.__bases__[-1]
|
appyClass = self.wrapperClass.__bases__[-1]
|
||||||
if hasattr(appyClass, 'workflow'):
|
if hasattr(appyClass, 'workflow'): wf = appyClass.workflow
|
||||||
workflowClass = appyClass.workflow
|
else: wf = WorkflowAnonymous
|
||||||
if workflowClass:
|
if not name: return wf
|
||||||
# Get the corresponding prototypical workflow instance
|
return WorkflowDescriptor.getWorkflowName(wf)
|
||||||
res = self.getProductConfig().workflowInstances[workflowClass]
|
|
||||||
else:
|
|
||||||
dcWorkflows = self.portal_workflow.getWorkflowsFor(self)
|
|
||||||
if dcWorkflows:
|
|
||||||
res = dcWorkflows[0]
|
|
||||||
return res
|
|
||||||
|
|
||||||
def getWorkflowLabel(self, stateName=None):
|
def getWorkflowLabel(self, stateName=None):
|
||||||
'''Gets the i18n label for the workflow current state. If no p_stateName
|
'''Gets the i18n label for p_stateName, or for the current object state
|
||||||
is given, workflow label is given for the current state.'''
|
if p_stateName is not given. Note that if p_stateName is given, it
|
||||||
res = ''
|
can also represent the name of a transition.'''
|
||||||
wf = self.getWorkflow(appy=False)
|
stateName = stateName or self.getState()
|
||||||
if wf:
|
return '%s_%s' % (self.getWorkflow(name=True), stateName)
|
||||||
res = stateName
|
|
||||||
if not res:
|
|
||||||
res = self.portal_workflow.getInfoFor(self, 'review_state')
|
|
||||||
appyWf = self.getWorkflow(appy=True)
|
|
||||||
if appyWf:
|
|
||||||
res = '%s_%s' % (wf.id, res)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def hasHistory(self):
|
def hasHistory(self):
|
||||||
'''Has this object an history?'''
|
'''Has this object an history?'''
|
||||||
|
@ -848,39 +845,6 @@ class BaseMixin:
|
||||||
return {'events': history[startNumber:startNumber+batchSize],
|
return {'events': history[startNumber:startNumber+batchSize],
|
||||||
'totalNumber': len(history)}
|
'totalNumber': len(history)}
|
||||||
|
|
||||||
def may(self, transitionName):
|
|
||||||
'''May the user execute transition named p_transitionName?'''
|
|
||||||
# Get the Appy workflow instance
|
|
||||||
workflow = self.getWorkflow()
|
|
||||||
res = False
|
|
||||||
if workflow:
|
|
||||||
# Get the corresponding Appy transition
|
|
||||||
transition = workflow._transitionsMapping[transitionName]
|
|
||||||
user = self.portal_membership.getAuthenticatedMember()
|
|
||||||
if isinstance(transition.condition, Role):
|
|
||||||
# It is a role. Transition may be triggered if the user has this
|
|
||||||
# role.
|
|
||||||
res = user.has_role(transition.condition.name, self)
|
|
||||||
elif type(transition.condition) == types.FunctionType:
|
|
||||||
res = transition.condition(workflow, self.appy())
|
|
||||||
elif type(transition.condition) in (tuple, list):
|
|
||||||
# It is a list of roles and or functions. Transition may be
|
|
||||||
# triggered if user has at least one of those roles and if all
|
|
||||||
# functions return True.
|
|
||||||
hasRole = None
|
|
||||||
for roleOrFunction in transition.condition:
|
|
||||||
if isinstance(roleOrFunction, basestring):
|
|
||||||
if hasRole == None:
|
|
||||||
hasRole = False
|
|
||||||
if user.has_role(roleOrFunction, self):
|
|
||||||
hasRole = True
|
|
||||||
elif type(roleOrFunction) == types.FunctionType:
|
|
||||||
if not roleOrFunction(workflow, self.appy()):
|
|
||||||
return False
|
|
||||||
if hasRole != False:
|
|
||||||
res = True
|
|
||||||
return res
|
|
||||||
|
|
||||||
def mayNavigate(self):
|
def mayNavigate(self):
|
||||||
'''May the currently logged user see the navigation panel linked to
|
'''May the currently logged user see the navigation panel linked to
|
||||||
this object?'''
|
this object?'''
|
||||||
|
@ -946,16 +910,28 @@ class BaseMixin:
|
||||||
# the user.
|
# the user.
|
||||||
return self.goto(msg)
|
return self.goto(msg)
|
||||||
|
|
||||||
def onTriggerTransition(self):
|
def do(self, transitionName, comment='', doAction=True, doNotify=True,
|
||||||
|
doHistory=True, doSay=True):
|
||||||
|
'''Triggers transition named p_transitionName.'''
|
||||||
|
# Check that this transition exists.
|
||||||
|
wf = self.getWorkflow()
|
||||||
|
if not hasattr(wf, transitionName) or \
|
||||||
|
getattr(wf, transitionName).__class__.__name__ != 'Transition':
|
||||||
|
raise 'Transition "%s" was not found.' % transitionName
|
||||||
|
# Is this transition triggerable?
|
||||||
|
transition = getattr(wf, transitionName)
|
||||||
|
if not transition.isTriggerable(self, wf):
|
||||||
|
raise 'Transition "%s" can\'t be triggered' % transitionName
|
||||||
|
# Trigger the transition
|
||||||
|
transition.trigger(transitionName, self, wf, comment, doAction=doAction,
|
||||||
|
doNotify=doNotify, doHistory=doHistory, doSay=doSay)
|
||||||
|
|
||||||
|
def onDo(self):
|
||||||
'''This method is called whenever a user wants to trigger a workflow
|
'''This method is called whenever a user wants to trigger a workflow
|
||||||
transition on an object.'''
|
transition on an object.'''
|
||||||
rq = self.REQUEST
|
rq = self.REQUEST
|
||||||
self.portal_workflow.doActionFor(self, rq['workflow_action'],
|
self.do(rq['workflow_action'], comment=rq.get('comment', ''))
|
||||||
comment = rq.get('comment', ''))
|
|
||||||
self.reindexObject()
|
self.reindexObject()
|
||||||
# Where to redirect the user back ?
|
|
||||||
# TODO (?): remove the "phase" param for redirecting the user to the
|
|
||||||
# next phase when relevant.
|
|
||||||
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
return self.goto(self.getUrl(rq['HTTP_REFERER']))
|
||||||
|
|
||||||
def fieldValueSelected(self, fieldName, vocabValue, dbValue):
|
def fieldValueSelected(self, fieldName, vocabValue, dbValue):
|
||||||
|
|
|
@ -34,10 +34,10 @@ SENDMAIL_ERROR = 'Error while sending mail: %s.'
|
||||||
ENCODING_ERROR = 'Encoding error while sending mail: %s.'
|
ENCODING_ERROR = 'Encoding error while sending mail: %s.'
|
||||||
|
|
||||||
from appy.gen.utils import sequenceTypes
|
from appy.gen.utils import sequenceTypes
|
||||||
from appy.gen.plone25.descriptors import WorkflowDescriptor
|
from appy.gen.descriptors import WorkflowDescriptor
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
def sendMail(obj, transition, transitionName, workflow, logger):
|
def sendMail(obj, transition, transitionName, workflow):
|
||||||
'''Sends mail about p_transition that has been triggered on p_obj that is
|
'''Sends mail about p_transition that has been triggered on p_obj that is
|
||||||
controlled by p_workflow.'''
|
controlled by p_workflow.'''
|
||||||
wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__)
|
wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__)
|
||||||
|
@ -94,12 +94,12 @@ def sendMail(obj, transition, transitionName, workflow, logger):
|
||||||
recipient.encode(enc), fromAddress.encode(enc),
|
recipient.encode(enc), fromAddress.encode(enc),
|
||||||
mailSubject.encode(enc), mcc=cc, charset='utf-8')
|
mailSubject.encode(enc), mcc=cc, charset='utf-8')
|
||||||
except socket.error, sg:
|
except socket.error, sg:
|
||||||
logger.warn(SENDMAIL_ERROR % str(sg))
|
obj.log(SENDMAIL_ERROR % str(sg), type='warning')
|
||||||
break
|
break
|
||||||
except UnicodeDecodeError, ue:
|
except UnicodeDecodeError, ue:
|
||||||
logger.warn(ENCODING_ERROR % str(ue))
|
obj.log(ENCODING_ERROR % str(ue), type='warning')
|
||||||
break
|
break
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logger.warn(SENDMAIL_ERROR % str(e))
|
obj.log(SENDMAIL_ERROR % str(e), type='warning')
|
||||||
break
|
break
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -515,7 +515,7 @@
|
||||||
tal:condition="transitions">
|
tal:condition="transitions">
|
||||||
<form id="triggerTransitionForm" method="post"
|
<form id="triggerTransitionForm" method="post"
|
||||||
tal:attributes="action python: contextObj.absolute_url() + '/skyn/do'">
|
tal:attributes="action python: contextObj.absolute_url() + '/skyn/do'">
|
||||||
<input type="hidden" name="action" value="TriggerTransition"/>
|
<input type="hidden" name="action" value="Do"/>
|
||||||
<input type="hidden" name="workflow_action"/>
|
<input type="hidden" name="workflow_action"/>
|
||||||
<table>
|
<table>
|
||||||
<tr valign="middle">
|
<tr valign="middle">
|
||||||
|
@ -530,11 +530,11 @@
|
||||||
<td align="right" tal:repeat="transition transitions">
|
<td align="right" tal:repeat="transition transitions">
|
||||||
<tal:comment replace="nothing">Real button</tal:comment>
|
<tal:comment replace="nothing">Real button</tal:comment>
|
||||||
<input type="button" class="appyButton" tal:condition="transition/may_trigger"
|
<input type="button" class="appyButton" tal:condition="transition/may_trigger"
|
||||||
tal:attributes="value python: tool.translate(transition['name']);
|
tal:attributes="value transition/title;
|
||||||
onClick python: 'triggerTransition(\'%s\',\'%s\')' % (transition['id'],transition['confirm']);"/>
|
onClick python: 'triggerTransition(\'%s\',\'%s\')' % (transition['name'],transition['confirm']);"/>
|
||||||
<tal:comment replace="nothing">Fake button, explaining why the transition can't be triggered</tal:comment>
|
<tal:comment replace="nothing">Fake button, explaining why the transition can't be triggered</tal:comment>
|
||||||
<div class="appyButton fakeButton" tal:condition="not: transition/may_trigger">
|
<div class="appyButton fakeButton" tal:condition="not: transition/may_trigger">
|
||||||
<acronym tal:content="python: tool.translate(transition['name'])"
|
<acronym tal:content="transition/title"
|
||||||
tal:attributes="title transition/reason"></acronym>
|
tal:attributes="title transition/reason"></acronym>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -27,7 +27,6 @@ from Products.CMFPlone.utils import ToolInit
|
||||||
from Products.CMFPlone.interfaces import IPloneSiteRoot
|
from Products.CMFPlone.interfaces import IPloneSiteRoot
|
||||||
from Products.CMFCore import DirectoryView
|
from Products.CMFCore import DirectoryView
|
||||||
from Products.CMFCore.DirectoryView import manage_addDirectoryView
|
from Products.CMFCore.DirectoryView import manage_addDirectoryView
|
||||||
from Products.DCWorkflow.Transitions import TRIGGER_USER_ACTION
|
|
||||||
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
||||||
from Products.Archetypes.Extensions.utils import installTypes
|
from Products.Archetypes.Extensions.utils import installTypes
|
||||||
from Products.Archetypes.Extensions.utils import install_subskin
|
from Products.Archetypes.Extensions.utils import install_subskin
|
||||||
|
@ -57,17 +56,6 @@ allClassNames = [<!allClassNames!>]
|
||||||
catalogMap = {}
|
catalogMap = {}
|
||||||
<!catalogMap!>
|
<!catalogMap!>
|
||||||
|
|
||||||
# Dict whose keys are class names and whose values are workflow names (=the
|
|
||||||
# workflow used by the content type)
|
|
||||||
workflows = {<!workflows!>}
|
|
||||||
# In the following dict, we keep one instance for every Appy workflow defined
|
|
||||||
# in the application. Those prototypical instances will be used for executing
|
|
||||||
# user-defined actions and transitions. For each instance, we add a special
|
|
||||||
# attribute "_transitionsMapping" that allows to get Appy transitions from the
|
|
||||||
# names of DC transitions.
|
|
||||||
workflowInstances = {}
|
|
||||||
<!workflowInstancesInit!>
|
|
||||||
|
|
||||||
# In the following dict, we store, for every Appy class, the ordered list of
|
# In the following dict, we store, for every Appy class, the ordered list of
|
||||||
# appy types (included inherited ones).
|
# appy types (included inherited ones).
|
||||||
attributes = {<!attributes!>}
|
attributes = {<!attributes!>}
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<!codeHeader!>
|
|
||||||
from Products.CMFCore.WorkflowTool import addWorkflowFactory
|
|
||||||
from Products.DCWorkflow.DCWorkflow import DCWorkflowDefinition
|
|
||||||
from appy.gen.plone25.workflow import WorkflowCreator
|
|
||||||
from Products.<!applicationName!>.config import PROJECTNAME
|
|
||||||
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger('<!applicationName!>')
|
|
||||||
from appy.gen.plone25.workflow import do
|
|
||||||
|
|
||||||
<!workflows!>
|
|
|
@ -1,186 +0,0 @@
|
||||||
'''This package contains functions for managing workflow events.'''
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
class WorkflowCreator:
|
|
||||||
'''This class allows to construct the Plone workflow that corresponds to a
|
|
||||||
Appy workflow.'''
|
|
||||||
|
|
||||||
def __init__(self, wfName, ploneWorkflowClass, stateNames, initialState,
|
|
||||||
stateInfos, transitionNames, transitionInfos, managedPermissions,
|
|
||||||
productName, externalMethodClass):
|
|
||||||
self.wfName = wfName
|
|
||||||
self.ploneWorkflowClass = ploneWorkflowClass
|
|
||||||
self.stateNames = stateNames
|
|
||||||
self.initialState = initialState # Name of the initial state
|
|
||||||
self.stateInfos = stateInfos
|
|
||||||
# stateInfos is a dict giving information about every state. Keys are
|
|
||||||
# state names, values are lists? Every list contains (in this order):
|
|
||||||
# - the list of transitions (names) going out from this state;
|
|
||||||
# - a dict of permissions, whose keys are permission names and whose
|
|
||||||
# values are lists of roles that are granted this permission. In
|
|
||||||
# short: ~{s_stateName: ([transitions], {s_permissionName:
|
|
||||||
# (roleNames)})}~.
|
|
||||||
self.transitionNames = transitionNames
|
|
||||||
self.transitionInfos = transitionInfos
|
|
||||||
# transitionInfos is a dict giving information avout every transition.
|
|
||||||
# Keys are transition names, values are end states of the transitions.
|
|
||||||
self.variableInfos = {
|
|
||||||
'review_history': ("Provides access to workflow history",
|
|
||||||
'state_change/getHistory', 0, 0, {'guard_permissions':\
|
|
||||||
'Request review; Review portal content'}),
|
|
||||||
'comments': ("Comments about the last transition",
|
|
||||||
'python:state_change.kwargs.get("comment", "")', 1, 1, None),
|
|
||||||
'time': ("Time of the last transition", "state_change/getDateTime",
|
|
||||||
1, 1, None),
|
|
||||||
'actor': ("The ID of the user who performed the last transition",
|
|
||||||
"user/getId", 1, 1, None),
|
|
||||||
'action': ("The last transition", "transition/getId|nothing",
|
|
||||||
1, 1, None)
|
|
||||||
}
|
|
||||||
self.managedPermissions = managedPermissions
|
|
||||||
self.ploneWf = None # The Plone DC workflow definition
|
|
||||||
self.productName = productName
|
|
||||||
self.externalMethodClass = externalMethodClass
|
|
||||||
|
|
||||||
def createWorkflowDefinition(self):
|
|
||||||
'''Creates the Plone instance corresponding to this workflow.'''
|
|
||||||
self.ploneWf = self.ploneWorkflowClass(self.wfName)
|
|
||||||
self.ploneWf.setProperties(title=self.wfName)
|
|
||||||
|
|
||||||
def createWorkflowElements(self):
|
|
||||||
'''Creates states, transitions, variables and managed permissions and
|
|
||||||
sets the initial state.'''
|
|
||||||
wf = self.ploneWf
|
|
||||||
# Create states
|
|
||||||
for s in self.stateNames:
|
|
||||||
try:
|
|
||||||
wf.states[s]
|
|
||||||
except KeyError, k:
|
|
||||||
# It does not exist, so we create it!
|
|
||||||
wf.states.addState(s)
|
|
||||||
# Create transitions
|
|
||||||
for t in self.transitionNames:
|
|
||||||
try:
|
|
||||||
wf.transitions[t]
|
|
||||||
except KeyError, k:
|
|
||||||
wf.transitions.addTransition(t)
|
|
||||||
# Create variables
|
|
||||||
for v in self.variableInfos.iterkeys():
|
|
||||||
try:
|
|
||||||
wf.variables[v]
|
|
||||||
except KeyError, k:
|
|
||||||
wf.variables.addVariable(v)
|
|
||||||
# Create managed permissions
|
|
||||||
for mp in self.managedPermissions:
|
|
||||||
try:
|
|
||||||
wf.addManagedPermission(mp)
|
|
||||||
except ValueError, va:
|
|
||||||
pass # Already a managed permission
|
|
||||||
# Set initial state
|
|
||||||
if not wf.initial_state: wf.states.setInitialState(self.initialState)
|
|
||||||
|
|
||||||
def getTransitionScriptName(self, transitionName):
|
|
||||||
'''Gets the name of the script corresponding to DC p_transitionName.'''
|
|
||||||
return '%s_do%s%s' % (self.wfName, transitionName[0].upper(),
|
|
||||||
transitionName[1:])
|
|
||||||
|
|
||||||
def configureStatesAndTransitions(self):
|
|
||||||
'''Configures states and transitions of the Plone workflow.'''
|
|
||||||
wf = self.ploneWf
|
|
||||||
# Configure states
|
|
||||||
for stateName, stateInfo in self.stateInfos.iteritems():
|
|
||||||
state = wf.states[stateName]
|
|
||||||
stateTitle = '%s_%s' % (self.wfName, stateName)
|
|
||||||
state.setProperties(title=stateTitle, description="",
|
|
||||||
transitions=stateInfo[0])
|
|
||||||
for permissionName, roles in stateInfo[1].iteritems():
|
|
||||||
state.setPermission(permissionName, 0, roles)
|
|
||||||
# Configure transitions
|
|
||||||
for transitionName, endStateName in self.transitionInfos.iteritems():
|
|
||||||
# Define the script to call when the transition has been triggered.
|
|
||||||
scriptName = self.getTransitionScriptName(transitionName)
|
|
||||||
if not scriptName in wf.scripts.objectIds():
|
|
||||||
sn = scriptName
|
|
||||||
wf.scripts._setObject(sn, self.externalMethodClass(
|
|
||||||
sn, sn, self.productName + '.workflows', sn))
|
|
||||||
# Configure the transition in itself
|
|
||||||
transition = wf.transitions[transitionName]
|
|
||||||
transition.setProperties(
|
|
||||||
title=transitionName, new_state_id=endStateName, trigger_type=1,
|
|
||||||
script_name="", after_script_name=scriptName,
|
|
||||||
actbox_name='%s_%s' % (self.wfName, transitionName),
|
|
||||||
actbox_url="",
|
|
||||||
props={'guard_expr': 'python:here.may("%s")' % transitionName})
|
|
||||||
|
|
||||||
def configureVariables(self):
|
|
||||||
'''Configures the variables defined in this workflow.'''
|
|
||||||
wf = self.ploneWf
|
|
||||||
# Set the name of the state variable
|
|
||||||
wf.variables.setStateVar('review_state')
|
|
||||||
# Configure the variables
|
|
||||||
for variableName, info in self.variableInfos.iteritems():
|
|
||||||
var = wf.variables[variableName]
|
|
||||||
var.setProperties(description=info[0], default_value='',
|
|
||||||
default_expr=info[1], for_catalog=0, for_status=info[2],
|
|
||||||
update_always=info[3], props=info[4])
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.createWorkflowDefinition()
|
|
||||||
self.createWorkflowElements()
|
|
||||||
self.configureStatesAndTransitions()
|
|
||||||
self.configureVariables()
|
|
||||||
return self.ploneWf
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
import notifier
|
|
||||||
def do(transitionName, stateChange, logger):
|
|
||||||
'''This function is called by a Plone workflow every time a transition named
|
|
||||||
p_transitionName has been triggered. p_stateChange.objet is the Plone
|
|
||||||
object on which the transition has been triggered; p_logger is the Zope
|
|
||||||
logger allowing to dump information, warnings or errors in the log file
|
|
||||||
or object.'''
|
|
||||||
ploneObj = stateChange.object
|
|
||||||
workflow = ploneObj.getWorkflow()
|
|
||||||
transition = workflow._transitionsMapping[transitionName]
|
|
||||||
msg = ''
|
|
||||||
# Must I execute transition-related actions and notifications?
|
|
||||||
doAction = False
|
|
||||||
if transition.action:
|
|
||||||
doAction = True
|
|
||||||
if hasattr(ploneObj, '_appy_do') and \
|
|
||||||
not ploneObj._appy_do['doAction']:
|
|
||||||
doAction = False
|
|
||||||
doNotify = False
|
|
||||||
if transition.notify:
|
|
||||||
doNotify = True
|
|
||||||
if hasattr(ploneObj, '_appy_do') and \
|
|
||||||
not ploneObj._appy_do['doNotify']:
|
|
||||||
doNotify = False
|
|
||||||
elif not getattr(ploneObj.getTool().appy(), 'enableNotifications'):
|
|
||||||
# We do not notify if the "notify" flag in the tool is disabled.
|
|
||||||
doNotify = False
|
|
||||||
if doAction or doNotify:
|
|
||||||
obj = ploneObj.appy()
|
|
||||||
if doAction:
|
|
||||||
msg = ''
|
|
||||||
if type(transition.action) in (tuple, list):
|
|
||||||
# We need to execute a list of actions
|
|
||||||
for act in transition.action:
|
|
||||||
msgPart = act(workflow, obj)
|
|
||||||
if msgPart: msg += msgPart
|
|
||||||
else: # We execute a single action only.
|
|
||||||
msgPart = transition.action(workflow, obj)
|
|
||||||
if msgPart: msg += msgPart
|
|
||||||
if doNotify:
|
|
||||||
notifier.sendMail(obj, transition, transitionName, workflow, logger)
|
|
||||||
# Produce a message to the user
|
|
||||||
if hasattr(ploneObj, '_appy_do') and not ploneObj._appy_do['doSay']:
|
|
||||||
# We do not produce any message if the transition was triggered
|
|
||||||
# programmatically.
|
|
||||||
return
|
|
||||||
# Produce a default message if no transition has given a custom one.
|
|
||||||
if not msg:
|
|
||||||
msg = ploneObj.translate(u'Your content\'s status has been modified.',
|
|
||||||
domain='plone')
|
|
||||||
ploneObj.say(msg)
|
|
||||||
# ------------------------------------------------------------------------------
|
|
|
@ -37,7 +37,7 @@ class AbstractWrapper:
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
if other: return cmp(self.o, other.o)
|
if other: return cmp(self.o, other.o)
|
||||||
else: return 1
|
return 1
|
||||||
|
|
||||||
def _callCustom(self, methodName, *args, **kwargs):
|
def _callCustom(self, methodName, *args, **kwargs):
|
||||||
'''This wrapper implements some methods like "validate" and "onEdit".
|
'''This wrapper implements some methods like "validate" and "onEdit".
|
||||||
|
@ -68,8 +68,13 @@ class AbstractWrapper:
|
||||||
def get_uid(self): return self.o.UID()
|
def get_uid(self): return self.o.UID()
|
||||||
uid = property(get_uid)
|
uid = property(get_uid)
|
||||||
|
|
||||||
def get_state(self):
|
def get_klass(self): return self.__class__.__bases__[-1]
|
||||||
return self.o.portal_workflow.getInfoFor(self.o, 'review_state')
|
klass = property(get_klass)
|
||||||
|
|
||||||
|
def get_url(self): return self.o.absolute_url()
|
||||||
|
url = property(get_url)
|
||||||
|
|
||||||
|
def get_state(self): return self.o.getState()
|
||||||
state = property(get_state)
|
state = property(get_state)
|
||||||
|
|
||||||
def get_stateLabel(self):
|
def get_stateLabel(self):
|
||||||
|
@ -77,12 +82,6 @@ class AbstractWrapper:
|
||||||
return self.o.translate(self.o.getWorkflowLabel(), domain=appName)
|
return self.o.translate(self.o.getWorkflowLabel(), domain=appName)
|
||||||
stateLabel = property(get_stateLabel)
|
stateLabel = property(get_stateLabel)
|
||||||
|
|
||||||
def get_klass(self): return self.__class__.__bases__[-1]
|
|
||||||
klass = property(get_klass)
|
|
||||||
|
|
||||||
def get_url(self): return self.o.absolute_url()
|
|
||||||
url = property(get_url)
|
|
||||||
|
|
||||||
def get_history(self):
|
def get_history(self):
|
||||||
key = self.o.workflow_history.keys()[0]
|
key = self.o.workflow_history.keys()[0]
|
||||||
return self.o.workflow_history[key]
|
return self.o.workflow_history[key]
|
||||||
|
@ -256,42 +255,12 @@ class AbstractWrapper:
|
||||||
return self.o.translate(label, mapping, domain, language=language,
|
return self.o.translate(label, mapping, domain, language=language,
|
||||||
format=format)
|
format=format)
|
||||||
|
|
||||||
def do(self, transition, comment='', doAction=False, doNotify=False,
|
def do(self, transition, comment='', doAction=True, doNotify=True,
|
||||||
doHistory=True):
|
doHistory=True):
|
||||||
'''This method allows to trigger on p_self a workflow p_transition
|
'''This method allows to trigger on p_self a workflow p_transition
|
||||||
programmatically. If p_doAction is False, the action that must
|
programmatically. See doc in self.o.do.'''
|
||||||
normally be executed after the transition has been triggered will
|
return self.o.do(transition, comment, doAction=doAction,
|
||||||
not be executed. If p_doNotify is False, the notifications
|
doNotify=doNotify, doHistory=doHistory, doSay=False)
|
||||||
(email,...) that must normally be launched after the transition has
|
|
||||||
been triggered will not be launched. If p_doHistory is False, there
|
|
||||||
will be no trace from this transition triggering in the workflow
|
|
||||||
history.'''
|
|
||||||
wfTool = self.o.portal_workflow
|
|
||||||
availableTransitions = [t['id'] for t in self.o.getAppyTransitions(\
|
|
||||||
includeFake=False, includeNotShowable=True)]
|
|
||||||
transitionName = transition
|
|
||||||
if not transitionName in availableTransitions:
|
|
||||||
# Maybe is it a compound Appy transition. Try to find the
|
|
||||||
# corresponding DC transition.
|
|
||||||
state = self.state
|
|
||||||
transitionPrefix = transition + state[0].upper() + state[1:] + 'To'
|
|
||||||
for at in availableTransitions:
|
|
||||||
if at.startswith(transitionPrefix):
|
|
||||||
transitionName = at
|
|
||||||
break
|
|
||||||
# Set in a versatile attribute details about what to execute or not
|
|
||||||
# (actions, notifications) after the transition has been executed by DC
|
|
||||||
# workflow.
|
|
||||||
self.o._appy_do = {'doAction': doAction, 'doNotify': doNotify,
|
|
||||||
'doSay': False}
|
|
||||||
if not doHistory:
|
|
||||||
comment = '_invisible_' # Will not be displayed.
|
|
||||||
# At first sight, I wanted to remove the entry from
|
|
||||||
# self.o.workflow_history. But Plone determines the state of an
|
|
||||||
# object by consulting the target state of the last transition in
|
|
||||||
# this workflow_history.
|
|
||||||
wfTool.doActionFor(self.o, transitionName, comment=comment)
|
|
||||||
del self.o._appy_do
|
|
||||||
|
|
||||||
def log(self, message, type='info'): return self.o.log(message, type)
|
def log(self, message, type='info'): return self.o.log(message, type)
|
||||||
def say(self, message, type='info'): return self.o.say(message, type)
|
def say(self, message, type='info'): return self.o.say(message, type)
|
||||||
|
|
|
@ -330,7 +330,7 @@ class XmlUnmarshaller(XmlParser):
|
||||||
if isinstance(currentContainer, list):
|
if isinstance(currentContainer, list):
|
||||||
currentContainer.append(value)
|
currentContainer.append(value)
|
||||||
elif isinstance(currentContainer, UnmarshalledFile):
|
elif isinstance(currentContainer, UnmarshalledFile):
|
||||||
currentContainer.content += value
|
currentContainer.content += value or ''
|
||||||
else:
|
else:
|
||||||
# Current container is an object
|
# Current container is an object
|
||||||
if hasattr(currentContainer, name) and \
|
if hasattr(currentContainer, name) and \
|
||||||
|
|
Loading…
Reference in a new issue