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
14 changed files with 378 additions and 806 deletions
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue