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 -*-
|
||||
# ------------------------------------------------------------------------------
|
||||
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 defaultFieldLayouts
|
||||
from appy.gen.po import PoMessage
|
||||
|
@ -2167,7 +2167,13 @@ class Pod(Type):
|
|||
value = value._atFile
|
||||
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:
|
||||
'''Represents a role.'''
|
||||
ploneRoles = ('Manager', 'Member', 'Owner', 'Reviewer', 'Anonymous',
|
||||
|
@ -2201,6 +2207,12 @@ class State:
|
|||
# Standardize the way roles are expressed within self.permissions
|
||||
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):
|
||||
'''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
|
||||
|
@ -2233,16 +2245,6 @@ class State:
|
|||
|
||||
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):
|
||||
'''If you get the permissions mapping through self.permissions, dict
|
||||
values may be of different types (a list of roles, a single role or
|
||||
|
@ -2258,6 +2260,38 @@ class State:
|
|||
res[permission] = roleValue
|
||||
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:
|
||||
def __init__(self, states, condition=True, action=None, notify=None,
|
||||
show=True, confirm=False):
|
||||
|
@ -2277,6 +2311,12 @@ class Transition:
|
|||
# the transition. It will only be possible by code.
|
||||
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):
|
||||
'''self.condition can specify a role.'''
|
||||
res = []
|
||||
|
@ -2296,23 +2336,6 @@ class Transition:
|
|||
else:
|
||||
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):
|
||||
'''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
|
||||
|
@ -2330,6 +2353,117 @@ class Transition:
|
|||
break
|
||||
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:
|
||||
'''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
|
||||
|
@ -2346,9 +2480,37 @@ class Permission:
|
|||
and "specificWritePermission" as booleans. When defining named
|
||||
(string) permissions, for referring to it you simply use those strings,
|
||||
you do not create instances of ReadPermission or WritePermission.'''
|
||||
|
||||
allowedChars = string.digits + string.letters + '_'
|
||||
|
||||
def __init__(self, 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 WritePermission(Permission): pass
|
||||
|
||||
|
@ -2363,6 +2525,17 @@ class No:
|
|||
def __nonzero__(self):
|
||||
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:
|
||||
'''Instances of this class may be given as validator of a String, in order
|
||||
|
|
|
@ -89,141 +89,9 @@ class ClassDescriptor(Descriptor):
|
|||
|
||||
class WorkflowDescriptor(Descriptor):
|
||||
'''This class gives information about an Appy workflow.'''
|
||||
|
||||
def _getWorkflowElements(self, elemType):
|
||||
res = []
|
||||
for attrName in dir(self.klass):
|
||||
attrValue = getattr(self.klass, attrName)
|
||||
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
|
||||
@staticmethod
|
||||
def getWorkflowName(klass):
|
||||
'''Returns the name of this workflow.'''
|
||||
res = klass.__module__.replace('.', '_') + '_' + klass.__name__
|
||||
return res.lower()
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -598,122 +598,4 @@ class TranslationClassDescriptor(ClassDescriptor):
|
|||
params['format'] = String.TEXT
|
||||
params['height'] = height
|
||||
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.generator import Generator as AbstractGenerator
|
||||
from appy.gen.utils import getClassName
|
||||
from descriptors import ClassDescriptor, WorkflowDescriptor, \
|
||||
ToolClassDescriptor, UserClassDescriptor, \
|
||||
TranslationClassDescriptor
|
||||
from appy.gen.descriptors import WorkflowDescriptor
|
||||
from descriptors import ClassDescriptor, ToolClassDescriptor, \
|
||||
UserClassDescriptor, TranslationClassDescriptor
|
||||
from model import ModelClass, User, Tool, Translation
|
||||
|
||||
# Common methods that need to be defined on every Archetype class --------------
|
||||
|
@ -38,7 +38,6 @@ class Generator(AbstractGenerator):
|
|||
AbstractGenerator.__init__(self, *args, **kwargs)
|
||||
# Set our own Descriptor classes
|
||||
self.descriptorClasses['class'] = ClassDescriptor
|
||||
self.descriptorClasses['workflow'] = WorkflowDescriptor
|
||||
# Create our own Tool, User and Translation instances
|
||||
self.tool = ToolClassDescriptor(Tool, self)
|
||||
self.user = UserClassDescriptor(User, self)
|
||||
|
@ -159,7 +158,6 @@ class Generator(AbstractGenerator):
|
|||
# Create basic files (config.py, Install.py, etc)
|
||||
self.generateTool()
|
||||
self.generateInit()
|
||||
self.generateWorkflows()
|
||||
self.generateTests()
|
||||
if self.config.frontPage:
|
||||
self.generateFrontPage()
|
||||
|
@ -368,33 +366,6 @@ class Generator(AbstractGenerator):
|
|||
catalogMap += "catalogMap['%s']['black'] = " \
|
||||
"['portal_catalog']\n" % blackClass
|
||||
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,
|
||||
# inherited included) for every Appy class.
|
||||
attributes = []
|
||||
|
@ -463,40 +434,6 @@ class Generator(AbstractGenerator):
|
|||
repls['totalNumberOfTests'] = self.totalNumberOfTests
|
||||
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):
|
||||
'''Generates the getter for attribute p_name.'''
|
||||
res = ' def get_%s(self):\n ' % name
|
||||
|
@ -519,18 +456,17 @@ class Generator(AbstractGenerator):
|
|||
* "custom" it includes descriptors for the config-related classes
|
||||
for which the user has created a sub-class.'''
|
||||
if not include: return self.classes
|
||||
else:
|
||||
res = self.classes[:]
|
||||
configClasses = [self.tool, self.user, self.translation]
|
||||
if include == 'all':
|
||||
res += configClasses
|
||||
elif include == 'allButTool':
|
||||
res += configClasses[1:]
|
||||
elif include == 'custom':
|
||||
res += [c for c in configClasses if c.customized]
|
||||
elif include == 'predefined':
|
||||
res = configClasses
|
||||
return res
|
||||
res = self.classes[:]
|
||||
configClasses = [self.tool, self.user, self.translation]
|
||||
if include == 'all':
|
||||
res += configClasses
|
||||
elif include == 'allButTool':
|
||||
res += configClasses[1:]
|
||||
elif include == 'custom':
|
||||
res += [c for c in configClasses if c.customized]
|
||||
elif include == 'predefined':
|
||||
res = configClasses
|
||||
return res
|
||||
|
||||
def getClassesInOrder(self, allClasses):
|
||||
'''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)
|
||||
|
||||
def generateWorkflow(self, wfDescr):
|
||||
'''This method does not generate the workflow definition, which is done
|
||||
in self.generateWorkflows. This method just creates the i18n labels
|
||||
related to the workflow described by p_wfDescr.'''
|
||||
'''This method creates the i18n labels related to the workflow described
|
||||
by p_wfDescr.'''
|
||||
k = wfDescr.klass
|
||||
print 'Generating %s.%s (gen-workflow)...' % (k.__module__, k.__name__)
|
||||
# Identify Plone workflow name
|
||||
# Identify workflow name
|
||||
wfName = WorkflowDescriptor.getWorkflowName(wfDescr.klass)
|
||||
# Add i18n messages for states and transitions
|
||||
for sName in wfDescr.getStateNames():
|
||||
poMsg = PoMessage('%s_%s' % (wfName, sName), '', sName)
|
||||
# Add i18n messages for states
|
||||
for name in dir(wfDescr.klass):
|
||||
if not isinstance(getattr(wfDescr.klass, name), State): continue
|
||||
poMsg = PoMessage('%s_%s' % (wfName, name), '', name)
|
||||
poMsg.produceNiceDefault()
|
||||
self.labels.append(poMsg)
|
||||
for tName, tLabel in wfDescr.getTransitionNames(withLabels=True):
|
||||
poMsg = PoMessage('%s_%s' % (wfName, tName), '', tLabel)
|
||||
# Add i18n messages for transitions
|
||||
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()
|
||||
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:
|
||||
# We need to generate a label for the message that will be shown
|
||||
# in the confirm popup.
|
||||
for tn in tNames:
|
||||
label = '%s_%s_confirm' % (wfName, tn)
|
||||
poMsg = PoMessage(label, '', PoMessage.CONFIRM)
|
||||
self.labels.append(poMsg)
|
||||
label = '%s_%s_confirm' % (wfName, name)
|
||||
poMsg = PoMessage(label, '', PoMessage.CONFIRM)
|
||||
self.labels.append(poMsg)
|
||||
if transition.notify:
|
||||
# Appy will send a mail when this transition is triggered.
|
||||
# So we need 2 i18n labels for every DC transition corresponding
|
||||
# to this Appy transition: one for the mail subject and one for
|
||||
# So we need 2 i18n labels: one for the mail subject and one for
|
||||
# the mail body.
|
||||
for tn in tNames:
|
||||
subjectLabel = '%s_%s_mail_subject' % (wfName, tn)
|
||||
poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT)
|
||||
self.labels.append(poMsg)
|
||||
bodyLabel = '%s_%s_mail_body' % (wfName, tn)
|
||||
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
|
||||
self.labels.append(poMsg)
|
||||
subjectLabel = '%s_%s_mail_subject' % (wfName, name)
|
||||
poMsg = PoMessage(subjectLabel, '', PoMessage.EMAIL_SUBJECT)
|
||||
self.labels.append(poMsg)
|
||||
bodyLabel = '%s_%s_mail_body' % (wfName, name)
|
||||
poMsg = PoMessage(bodyLabel, '', PoMessage.EMAIL_BODY)
|
||||
self.labels.append(poMsg)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -34,7 +34,6 @@ class PloneInstaller:
|
|||
self.catalogMap = cfg.catalogMap
|
||||
self.applicationRoles = cfg.applicationRoles # Roles defined in the app
|
||||
self.defaultAddRoles = cfg.defaultAddRoles
|
||||
self.workflows = cfg.workflows
|
||||
self.appFrontPage = cfg.appFrontPage
|
||||
self.showPortlet = cfg.showPortlet
|
||||
self.languages = cfg.languages
|
||||
|
@ -378,22 +377,6 @@ class PloneInstaller:
|
|||
site.portal_groups.setRolesForGroup(group, [role])
|
||||
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):
|
||||
'''Registers In Plone the stylesheet linked to this application.'''
|
||||
cssName = self.productName + '.css'
|
||||
|
@ -495,7 +478,6 @@ class PloneInstaller:
|
|||
self.installTool()
|
||||
self.installTranslations()
|
||||
self.installRolesAndGroups()
|
||||
self.installWorkflows()
|
||||
self.installStyleSheet()
|
||||
self.managePortlets()
|
||||
self.manageIndexes()
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys, types, mimetypes, urllib, cgi
|
||||
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.layout import Table, defaultPageLayouts
|
||||
from appy.gen.descriptors import WorkflowDescriptor
|
||||
from appy.gen.plone25.descriptors import ClassDescriptor
|
||||
from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard
|
||||
|
||||
|
@ -300,6 +302,27 @@ class BaseMixin:
|
|||
else: logMethod = logger.info
|
||||
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):
|
||||
'''This method is called before updating an object and remembers, for
|
||||
every historized field, the previous value. Result is a dict
|
||||
|
@ -327,11 +350,10 @@ class BaseMixin:
|
|||
else:
|
||||
changes[fieldName] = (changes[fieldName], appyType.labelId)
|
||||
# Create the event to record in the history
|
||||
DateTime = self.getProductConfig().DateTime
|
||||
state = self.portal_workflow.getInfoFor(self, 'review_state')
|
||||
from DateTime import DateTime
|
||||
user = self.portal_membership.getAuthenticatedMember()
|
||||
event = {'action': '_datachange_', 'changes': changes,
|
||||
'review_state': state, 'actor': user.id,
|
||||
'review_state': self.getState(), 'actor': user.id,
|
||||
'time': DateTime(), 'comments': ''}
|
||||
# Add the event to the history
|
||||
histKey = self.workflow_history.keys()[0]
|
||||
|
@ -583,62 +605,48 @@ class BaseMixin:
|
|||
'''Returns information about the states that are related to p_phase.
|
||||
If p_currentOnly is True, we return the current state, even if not
|
||||
related to p_phase.'''
|
||||
res = []
|
||||
dcWorkflow = self.getWorkflow(appy=False)
|
||||
if not dcWorkflow: return res
|
||||
currentState = self.portal_workflow.getInfoFor(self, 'review_state')
|
||||
currentState = self.getState()
|
||||
if currentOnly:
|
||||
return [StateDescr(currentState,'current').get()]
|
||||
workflow = self.getWorkflow(appy=True)
|
||||
if workflow:
|
||||
stateStatus = 'done'
|
||||
for stateName in workflow._states:
|
||||
if stateName == currentState:
|
||||
stateStatus = 'current'
|
||||
elif stateStatus != 'done':
|
||||
stateStatus = 'future'
|
||||
state = getattr(workflow, stateName)
|
||||
if (state.phase == phase) and \
|
||||
(self._appy_showState(workflow, state.show)):
|
||||
res.append(StateDescr(stateName, stateStatus).get())
|
||||
return [StateDescr(currentState, 'current').get()]
|
||||
res = []
|
||||
workflow = self.getWorkflow()
|
||||
stateStatus = 'done'
|
||||
for stateName in dir(workflow):
|
||||
if getattr(workflow, stateName).__class__.__name__ != 'State':
|
||||
continue
|
||||
if stateName == currentState:
|
||||
stateStatus = 'current'
|
||||
elif stateStatus != 'done':
|
||||
stateStatus = 'future'
|
||||
state = getattr(workflow, stateName)
|
||||
if (state.phase == phase) and \
|
||||
(self._appy_showState(workflow, state.show)):
|
||||
res.append(StateDescr(stateName, stateStatus).get())
|
||||
return res
|
||||
|
||||
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
||||
'''This method is similar to portal_workflow.getTransitionsFor, but:
|
||||
* is able (or not, depending on boolean p_includeFake) to retrieve
|
||||
transitions that the user can't trigger, but for which he needs to
|
||||
know for what reason he can't trigger it;
|
||||
* is able (or not, depending on p_includeNotShowable) to include
|
||||
transitions for which show=False at the Appy level. Indeed, because
|
||||
"showability" is only a GUI concern, and not a security concern,
|
||||
in some cases it has sense to set includeNotShowable=True, because
|
||||
those transitions are triggerable from a security point of view;
|
||||
* the transition-info is richer: it contains fake-related info (as
|
||||
described above) and confirm-related info (ie, when clicking on
|
||||
the button, do we ask the user to confirm via a popup?)'''
|
||||
'''This method returns info about transitions that one can trigger from
|
||||
the user interface.
|
||||
* if p_includeFake is True, it retrieves transitions that the user
|
||||
can't trigger, but for which he needs to know for what reason he
|
||||
can't trigger it;
|
||||
* if p_includeNotShowable is True, it includes transitions for which
|
||||
show=False. Indeed, because "showability" is only a GUI concern,
|
||||
and not a security concern, in some cases it has sense to set
|
||||
includeNotShowable=True, because those transitions are triggerable
|
||||
from a security point of view.
|
||||
'''
|
||||
res = []
|
||||
# Get some Plone stuff from the Plone-level config.py
|
||||
TRIGGER_USER_ACTION = self.getProductConfig().TRIGGER_USER_ACTION
|
||||
sm = self.getProductConfig().getSecurityManager
|
||||
# Get the workflow definition for p_obj.
|
||||
workflow = self.getWorkflow(appy=False)
|
||||
if not workflow: return res
|
||||
appyWorkflow = self.getWorkflow(appy=True)
|
||||
# What is the current state for this object?
|
||||
currentState = workflow._getWorkflowStateOf(self)
|
||||
if not currentState: return res
|
||||
# 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)
|
||||
wf = self.getWorkflow()
|
||||
currentState = self.getState(name=False)
|
||||
# Loop on every transition
|
||||
for name in dir(wf):
|
||||
transition = getattr(wf, name)
|
||||
if (transition.__class__.__name__ != 'Transition'): continue
|
||||
# Filter transitions that do not have currentState as start state
|
||||
if not transition.hasState(currentState, True): continue
|
||||
# Check if the transition can be triggered
|
||||
mayTrigger = transition.isTriggerable(self, wf)
|
||||
# Compute the condition that will lead to including or not this
|
||||
# transition
|
||||
if not includeFake:
|
||||
|
@ -646,19 +654,15 @@ class BaseMixin:
|
|||
else:
|
||||
includeIt = mayTrigger or isinstance(mayTrigger, No)
|
||||
if not includeNotShowable:
|
||||
includeIt = includeIt and appyTr.isShowable(appyWorkflow, self)
|
||||
includeIt = includeIt and transition.isShowable(wf, self)
|
||||
if not includeIt: continue
|
||||
# Add transition-info to the result.
|
||||
tInfo = {'id': transition.id, 'title': transition.title,
|
||||
'title_or_id': transition.title_or_id(),
|
||||
'description': transition.description, 'confirm': '',
|
||||
'name': transition.actbox_name, 'may_trigger': True,
|
||||
'url': transition.actbox_url %
|
||||
{'content_url': self.absolute_url(),
|
||||
'portal_url' : '', 'folder_url' : ''}}
|
||||
if appyTr.confirm:
|
||||
label = '%s_confirm' % tInfo['name']
|
||||
tInfo['confirm'] = self.translate(label, format='js')
|
||||
label = self.getWorkflowLabel(name)
|
||||
tInfo = {'name': name, 'title': self.translate(label),
|
||||
'confirm': '', 'may_trigger': True}
|
||||
if transition.confirm:
|
||||
cLabel = '%s_confirm' % label
|
||||
tInfo['confirm'] = self.translate(cLabel, format='js')
|
||||
if not mayTrigger:
|
||||
tInfo['may_trigger'] = False
|
||||
tInfo['reason'] = mayTrigger.msg
|
||||
|
@ -793,39 +797,32 @@ class BaseMixin:
|
|||
reverse = rq.get('reverse') == 'True'
|
||||
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
|
||||
|
||||
def getWorkflow(self, appy=True):
|
||||
'''Returns the Appy workflow instance that is relevant for this
|
||||
object. If p_appy is False, it returns the DC workflow.'''
|
||||
res = None
|
||||
if appy:
|
||||
# Get the workflow class first
|
||||
workflowClass = None
|
||||
if self.wrapperClass:
|
||||
appyClass = self.wrapperClass.__bases__[-1]
|
||||
if hasattr(appyClass, 'workflow'):
|
||||
workflowClass = appyClass.workflow
|
||||
if workflowClass:
|
||||
# Get the corresponding prototypical workflow instance
|
||||
res = self.getProductConfig().workflowInstances[workflowClass]
|
||||
else:
|
||||
dcWorkflows = self.portal_workflow.getWorkflowsFor(self)
|
||||
if dcWorkflows:
|
||||
res = dcWorkflows[0]
|
||||
return res
|
||||
def notifyWorkflowCreated(self):
|
||||
'''This method is called by Zope/CMF every time an object is created,
|
||||
be it temp or not. The objective here is to initialise workflow-
|
||||
related data on the object.'''
|
||||
wf = self.getWorkflow()
|
||||
# Get the initial workflow state
|
||||
initialState = self.getState(name=False)
|
||||
# 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]
|
||||
if hasattr(appyClass, 'workflow'): wf = appyClass.workflow
|
||||
else: wf = WorkflowAnonymous
|
||||
if not name: return wf
|
||||
return WorkflowDescriptor.getWorkflowName(wf)
|
||||
|
||||
def getWorkflowLabel(self, stateName=None):
|
||||
'''Gets the i18n label for the workflow current state. If no p_stateName
|
||||
is given, workflow label is given for the current state.'''
|
||||
res = ''
|
||||
wf = self.getWorkflow(appy=False)
|
||||
if wf:
|
||||
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
|
||||
'''Gets the i18n label for p_stateName, or for the current object state
|
||||
if p_stateName is not given. Note that if p_stateName is given, it
|
||||
can also represent the name of a transition.'''
|
||||
stateName = stateName or self.getState()
|
||||
return '%s_%s' % (self.getWorkflow(name=True), stateName)
|
||||
|
||||
def hasHistory(self):
|
||||
'''Has this object an history?'''
|
||||
|
@ -848,39 +845,6 @@ class BaseMixin:
|
|||
return {'events': history[startNumber:startNumber+batchSize],
|
||||
'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):
|
||||
'''May the currently logged user see the navigation panel linked to
|
||||
this object?'''
|
||||
|
@ -946,16 +910,28 @@ class BaseMixin:
|
|||
# the user.
|
||||
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
|
||||
transition on an object.'''
|
||||
rq = self.REQUEST
|
||||
self.portal_workflow.doActionFor(self, rq['workflow_action'],
|
||||
comment = rq.get('comment', ''))
|
||||
self.do(rq['workflow_action'], comment=rq.get('comment', ''))
|
||||
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']))
|
||||
|
||||
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.'
|
||||
|
||||
from appy.gen.utils import sequenceTypes
|
||||
from appy.gen.plone25.descriptors import WorkflowDescriptor
|
||||
from appy.gen.descriptors import WorkflowDescriptor
|
||||
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
|
||||
controlled by p_workflow.'''
|
||||
wfName = WorkflowDescriptor.getWorkflowName(workflow.__class__)
|
||||
|
@ -94,12 +94,12 @@ def sendMail(obj, transition, transitionName, workflow, logger):
|
|||
recipient.encode(enc), fromAddress.encode(enc),
|
||||
mailSubject.encode(enc), mcc=cc, charset='utf-8')
|
||||
except socket.error, sg:
|
||||
logger.warn(SENDMAIL_ERROR % str(sg))
|
||||
obj.log(SENDMAIL_ERROR % str(sg), type='warning')
|
||||
break
|
||||
except UnicodeDecodeError, ue:
|
||||
logger.warn(ENCODING_ERROR % str(ue))
|
||||
obj.log(ENCODING_ERROR % str(ue), type='warning')
|
||||
break
|
||||
except Exception, e:
|
||||
logger.warn(SENDMAIL_ERROR % str(e))
|
||||
obj.log(SENDMAIL_ERROR % str(e), type='warning')
|
||||
break
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -515,7 +515,7 @@
|
|||
tal:condition="transitions">
|
||||
<form id="triggerTransitionForm" method="post"
|
||||
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"/>
|
||||
<table>
|
||||
<tr valign="middle">
|
||||
|
@ -530,11 +530,11 @@
|
|||
<td align="right" tal:repeat="transition transitions">
|
||||
<tal:comment replace="nothing">Real button</tal:comment>
|
||||
<input type="button" class="appyButton" tal:condition="transition/may_trigger"
|
||||
tal:attributes="value python: tool.translate(transition['name']);
|
||||
onClick python: 'triggerTransition(\'%s\',\'%s\')' % (transition['id'],transition['confirm']);"/>
|
||||
tal:attributes="value transition/title;
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -27,7 +27,6 @@ from Products.CMFPlone.utils import ToolInit
|
|||
from Products.CMFPlone.interfaces import IPloneSiteRoot
|
||||
from Products.CMFCore import DirectoryView
|
||||
from Products.CMFCore.DirectoryView import manage_addDirectoryView
|
||||
from Products.DCWorkflow.Transitions import TRIGGER_USER_ACTION
|
||||
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
||||
from Products.Archetypes.Extensions.utils import installTypes
|
||||
from Products.Archetypes.Extensions.utils import install_subskin
|
||||
|
@ -57,17 +56,6 @@ allClassNames = [<!allClassNames!>]
|
|||
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
|
||||
# appy types (included inherited ones).
|
||||
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):
|
||||
if other: return cmp(self.o, other.o)
|
||||
else: return 1
|
||||
return 1
|
||||
|
||||
def _callCustom(self, methodName, *args, **kwargs):
|
||||
'''This wrapper implements some methods like "validate" and "onEdit".
|
||||
|
@ -68,8 +68,13 @@ class AbstractWrapper:
|
|||
def get_uid(self): return self.o.UID()
|
||||
uid = property(get_uid)
|
||||
|
||||
def get_state(self):
|
||||
return self.o.portal_workflow.getInfoFor(self.o, 'review_state')
|
||||
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_state(self): return self.o.getState()
|
||||
state = property(get_state)
|
||||
|
||||
def get_stateLabel(self):
|
||||
|
@ -77,12 +82,6 @@ class AbstractWrapper:
|
|||
return self.o.translate(self.o.getWorkflowLabel(), domain=appName)
|
||||
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):
|
||||
key = self.o.workflow_history.keys()[0]
|
||||
return self.o.workflow_history[key]
|
||||
|
@ -256,42 +255,12 @@ class AbstractWrapper:
|
|||
return self.o.translate(label, mapping, domain, language=language,
|
||||
format=format)
|
||||
|
||||
def do(self, transition, comment='', doAction=False, doNotify=False,
|
||||
def do(self, transition, comment='', doAction=True, doNotify=True,
|
||||
doHistory=True):
|
||||
'''This method allows to trigger on p_self a workflow p_transition
|
||||
programmatically. 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.'''
|
||||
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
|
||||
programmatically. See doc in self.o.do.'''
|
||||
return self.o.do(transition, comment, doAction=doAction,
|
||||
doNotify=doNotify, doHistory=doHistory, doSay=False)
|
||||
|
||||
def log(self, message, type='info'): return self.o.log(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):
|
||||
currentContainer.append(value)
|
||||
elif isinstance(currentContainer, UnmarshalledFile):
|
||||
currentContainer.content += value
|
||||
currentContainer.content += value or ''
|
||||
else:
|
||||
# Current container is an object
|
||||
if hasattr(currentContainer, name) and \
|
||||
|
|
Loading…
Reference in a new issue