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:
Gaetan Delannay 2011-07-26 22:15:04 +02:00
parent 93eb16670b
commit ddec7cd62c
14 changed files with 378 additions and 806 deletions

View file

@ -1 +1 @@
0.6.7 0.7.0

View file

@ -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

View file

@ -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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -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,18 +456,17 @@ 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': res += configClasses
res += configClasses elif include == 'allButTool':
elif include == 'allButTool': res += configClasses[1:]
res += configClasses[1:] elif include == 'custom':
elif include == 'custom': res += [c for c in configClasses if c.customized]
res += [c for c in configClasses if c.customized] elif include == 'predefined':
elif include == 'predefined': res = configClasses
res = configClasses return res
return res
def getClassesInOrder(self, allClasses): def getClassesInOrder(self, allClasses):
'''When generating wrappers, classes mut be dumped in order (else, it '''When generating wrappers, classes mut be dumped in order (else, it
@ -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, name)
bodyLabel = '%s_%s_mail_body' % (wfName, tn) poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY) self.labels.append(poMsg)
self.labels.append(poMsg)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -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()

View file

@ -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,62 +605,48 @@ 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 stateName == currentState: if getattr(workflow, stateName).__class__.__name__ != 'State':
stateStatus = 'current' continue
elif stateStatus != 'done': if stateName == currentState:
stateStatus = 'future' stateStatus = 'current'
state = getattr(workflow, stateName) elif stateStatus != 'done':
if (state.phase == phase) and \ stateStatus = 'future'
(self._appy_showState(workflow, state.show)): state = getattr(workflow, stateName)
res.append(StateDescr(stateName, stateStatus).get()) if (state.phase == phase) and \
(self._appy_showState(workflow, state.show)):
res.append(StateDescr(stateName, stateStatus).get())
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.
appyClass = self.wrapperClass.__bases__[-1] initialTransition = Transition((initialState, initialState))
if hasattr(appyClass, 'workflow'): initialTransition.trigger('_init_', self, wf, '')
workflowClass = appyClass.workflow
if workflowClass: def getWorkflow(self, name=False):
# Get the corresponding prototypical workflow instance '''Returns the workflow applicable for p_self (or its name, if p_name
res = self.getProductConfig().workflowInstances[workflowClass] is True).'''
else: appyClass = self.wrapperClass.__bases__[-1]
dcWorkflows = self.portal_workflow.getWorkflowsFor(self) if hasattr(appyClass, 'workflow'): wf = appyClass.workflow
if dcWorkflows: else: wf = WorkflowAnonymous
res = dcWorkflows[0] if not name: return wf
return res return WorkflowDescriptor.getWorkflowName(wf)
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):

View file

@ -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
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -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>

View file

@ -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!>}

View file

@ -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!>

View file

@ -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)
# ------------------------------------------------------------------------------

View file

@ -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)

View file

@ -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 \