Added the concept of 'fake' workflow transitions: when a user can't trigger a transition but needs an explanation about why he can't, a fake button is shown, with a explanation.
This commit is contained in:
parent
b48525c5bb
commit
77112c45be
|
@ -2054,6 +2054,13 @@ class Transition:
|
||||||
Else, returns False.'''
|
Else, returns False.'''
|
||||||
return isinstance(self.states[0], State)
|
return isinstance(self.states[0], State)
|
||||||
|
|
||||||
|
def isShowable(self, workflow, obj):
|
||||||
|
'''Is this transition showable?'''
|
||||||
|
if callable(self.show):
|
||||||
|
return self.show(workflow, obj.appy())
|
||||||
|
else:
|
||||||
|
return self.show
|
||||||
|
|
||||||
def getStates(self, fromStates=True):
|
def getStates(self, fromStates=True):
|
||||||
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
|
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
|
||||||
else. If you want to get the states grouped in tuples
|
else. If you want to get the states grouped in tuples
|
||||||
|
@ -2204,4 +2211,6 @@ class Config:
|
||||||
# define it, we will add a copy of the instance defined below.
|
# define it, we will add a copy of the instance defined below.
|
||||||
title = String(multiplicity=(1,1), show='edit')
|
title = String(multiplicity=(1,1), show='edit')
|
||||||
title.init('title', None, 'appy')
|
title.init('title', None, 'appy')
|
||||||
|
state = String()
|
||||||
|
state.init('state', None, 'appy')
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -24,6 +24,7 @@ class ToolMixin(BaseMixin):
|
||||||
'''Returns the name of the portal_type that is based on
|
'''Returns the name of the portal_type that is based on
|
||||||
p_metaTypeOrAppyType.'''
|
p_metaTypeOrAppyType.'''
|
||||||
appName = self.getProductConfig().PROJECTNAME
|
appName = self.getProductConfig().PROJECTNAME
|
||||||
|
res = metaTypeOrAppyClass
|
||||||
if not isinstance(metaTypeOrAppyClass, basestring):
|
if not isinstance(metaTypeOrAppyClass, basestring):
|
||||||
res = getClassName(metaTypeOrAppyClass, appName)
|
res = getClassName(metaTypeOrAppyClass, appName)
|
||||||
if res.find('Extensions_appyWrappers') != -1:
|
if res.find('Extensions_appyWrappers') != -1:
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
import os, os.path, sys, types, mimetypes, urllib
|
import os, os.path, sys, types, mimetypes, urllib
|
||||||
import appy.gen
|
import appy.gen
|
||||||
from appy.gen import Type, String, Selection, Role
|
from appy.gen import Type, String, Selection, Role, No
|
||||||
from appy.gen.utils import *
|
from appy.gen.utils import *
|
||||||
from appy.gen.layout import Table, defaultPageLayouts
|
from appy.gen.layout import Table, defaultPageLayouts
|
||||||
from appy.gen.plone25.descriptors import ClassDescriptor
|
from appy.gen.plone25.descriptors import ClassDescriptor
|
||||||
from appy.gen.plone25.utils import updateRolesForPermission
|
from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
class BaseMixin:
|
class BaseMixin:
|
||||||
|
@ -342,6 +342,7 @@ class BaseMixin:
|
||||||
|
|
||||||
def showField(self, name, layoutType='view'):
|
def showField(self, name, layoutType='view'):
|
||||||
'''Must I show field named p_name on this p_layoutType ?'''
|
'''Must I show field named p_name on this p_layoutType ?'''
|
||||||
|
if name == 'state': return False
|
||||||
return self.getAppyType(name).isShowable(self, layoutType)
|
return self.getAppyType(name).isShowable(self, layoutType)
|
||||||
|
|
||||||
def getMethod(self, methodName):
|
def getMethod(self, methodName):
|
||||||
|
@ -560,24 +561,66 @@ class BaseMixin:
|
||||||
res.append(StateDescr(stateName, stateStatus).get())
|
res.append(StateDescr(stateName, stateStatus).get())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getAppyTransitions(self):
|
def getAppyTransitions(self, includeFake=True, includeNotShowable=False):
|
||||||
'''Returns the transitions that the user can trigger on p_self.'''
|
'''This method is similar to portal_workflow.getTransitionsFor, but:
|
||||||
transitions = self.portal_workflow.getTransitionsFor(self)
|
* 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?)'''
|
||||||
res = []
|
res = []
|
||||||
if transitions:
|
# Get some Plone stuff from the Plone-level config.py
|
||||||
# Retrieve the corresponding Appy transition, to check if the user
|
TRIGGER_USER_ACTION = self.getProductConfig().TRIGGER_USER_ACTION
|
||||||
# may view it.
|
sm = self.getProductConfig().getSecurityManager
|
||||||
workflow = self.getWorkflow(appy=True)
|
# Get the workflow definition for p_obj.
|
||||||
if not workflow: return transitions
|
workflow = self.getWorkflow(appy=False)
|
||||||
for transition in transitions:
|
if not workflow: return res
|
||||||
# Get the corresponding Appy transition
|
appyWorkflow = self.getWorkflow(appy=True)
|
||||||
appyTr = workflow._transitionsMapping[transition['id']]
|
# What is the current state for this object?
|
||||||
if self._appy_showTransition(workflow, appyTr.show):
|
currentState = workflow._getWorkflowStateOf(self)
|
||||||
transition['confirm'] = ''
|
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)
|
||||||
|
# Compute the condition that will lead to including or not this
|
||||||
|
# transition
|
||||||
|
if not includeFake:
|
||||||
|
includeIt = mayTrigger
|
||||||
|
else:
|
||||||
|
includeIt = mayTrigger or isinstance(mayTrigger, No)
|
||||||
|
if not includeNotShowable:
|
||||||
|
includeIt = includeIt and appyTr.isShowable(appyWorkflow, 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:
|
if appyTr.confirm:
|
||||||
label = '%s_confirm' % transition['name']
|
label = '%s_confirm' % tInfo['name']
|
||||||
transition['confirm']=self.translate(label, format='js')
|
tInfo['confirm'] = self.translate(label, format='js')
|
||||||
res.append(transition)
|
if not mayTrigger:
|
||||||
|
tInfo['may_trigger'] = False
|
||||||
|
tInfo['reason'] = mayTrigger.msg
|
||||||
|
res.append(tInfo)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def getAppyPhases(self, currentOnly=False, layoutType='view'):
|
def getAppyPhases(self, currentOnly=False, layoutType='view'):
|
||||||
|
@ -947,12 +990,6 @@ class BaseMixin:
|
||||||
return stateShow(workflow, self.appy())
|
return stateShow(workflow, self.appy())
|
||||||
else: return stateShow
|
else: return stateShow
|
||||||
|
|
||||||
def _appy_showTransition(self, workflow, transitionShow):
|
|
||||||
'''Must I show a transition whose "show value" is p_transitionShow?'''
|
|
||||||
if callable(transitionShow):
|
|
||||||
return transitionShow(workflow, self.appy())
|
|
||||||
else: return transitionShow
|
|
||||||
|
|
||||||
def _appy_managePermissions(self):
|
def _appy_managePermissions(self):
|
||||||
'''When an object is created or updated, we must update "add"
|
'''When an object is created or updated, we must update "add"
|
||||||
permissions accordingly: if the object is a folder, we must set on
|
permissions accordingly: if the object is a folder, we must set on
|
||||||
|
|
BIN
gen/plone25/skin/fakeTransition.gif
Normal file
BIN
gen/plone25/skin/fakeTransition.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 B |
|
@ -528,9 +528,15 @@
|
||||||
|
|
||||||
<tal:comment replace="nothing">Buttons for triggering transitions</tal:comment>
|
<tal:comment replace="nothing">Buttons for triggering transitions</tal:comment>
|
||||||
<td align="right" tal:repeat="transition transitions">
|
<td align="right" tal:repeat="transition transitions">
|
||||||
<input type="button" class="appyButton"
|
<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']);
|
tal:attributes="value python: tool.translate(transition['name']);
|
||||||
onClick python: 'triggerTransition(\'%s\',\'%s\')' % (transition['id'],transition['confirm']);"/>
|
onClick python: 'triggerTransition(\'%s\',\'%s\')' % (transition['id'],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'])"
|
||||||
|
tal:attributes="title transition/reason"></acronym>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -34,10 +34,9 @@
|
||||||
</tal:publishedObject>
|
</tal:publishedObject>
|
||||||
|
|
||||||
<tal:comment replace="nothing">Create a section for every root class.</tal:comment>
|
<tal:comment replace="nothing">Create a section for every root class.</tal:comment>
|
||||||
<tal:section repeat="rootClass rootClasses">
|
<tal:section repeat="rootClass python: [rc for rc in rootClasses if tool.userMaySearch(rc)]">
|
||||||
<tal:comment replace="nothing">Section title, with action icons</tal:comment>
|
<tal:comment replace="nothing">Section title, with action icons</tal:comment>
|
||||||
<dt tal:condition="python: tool.userMaySearch(rootClass)"
|
<dt tal:attributes="class python:test((repeat['rootClass'].number()==1) and not contextObj, 'portletAppyItem', 'portletAppyItem portletSep')">
|
||||||
tal:attributes="class python:test((repeat['rootClass'].number()==1) and not contextObj, 'portletAppyItem', 'portletAppyItem portletSep')">
|
|
||||||
<table width="100%" cellspacing="0" cellpadding="0" class="no-style-table">
|
<table width="100%" cellspacing="0" cellpadding="0" class="no-style-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -91,12 +91,12 @@
|
||||||
|
|
||||||
<tal:comment replace="nothing">Workflow state</tal:comment>
|
<tal:comment replace="nothing">Workflow state</tal:comment>
|
||||||
<td id="field_workflow_state"
|
<td id="field_workflow_state"
|
||||||
tal:condition="python: widget['name'] == 'workflowState'"
|
tal:condition="python: widget['name'] == 'state'"
|
||||||
tal:content="python: tool.translate(obj.getWorkflowLabel())">
|
tal:content="python: tool.translate(obj.getWorkflowLabel())">
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<tal:comment replace="nothing">Any other field</tal:comment>
|
<tal:comment replace="nothing">Any other field</tal:comment>
|
||||||
<td tal:condition="python: widget['name'] not in ('title', 'workflowState')"
|
<td tal:condition="python: widget['name'] not in ('title', 'state')"
|
||||||
tal:attributes="id python:'field_%s' % widget['name']">
|
tal:attributes="id python:'field_%s' % widget['name']">
|
||||||
<tal:field define="contextObj python:obj;
|
<tal:field define="contextObj python:obj;
|
||||||
layoutType python:'cell';
|
layoutType python:'cell';
|
||||||
|
|
|
@ -180,6 +180,11 @@ th {
|
||||||
/* overflow: visible; IE produces ugly results with this */
|
/* overflow: visible; IE produces ugly results with this */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fakeButton {
|
||||||
|
background: #ffd5c0 url(&dtml-portal_url;/skyn/fakeTransition.gif) 5px 1px no-repeat;
|
||||||
|
padding: 3px 4px 3px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.listing { margin: 0em 0em; }
|
.listing { margin: 0em 0em; }
|
||||||
.listing td, .stx table td {
|
.listing td, .stx table td {
|
||||||
padding : 0.1em 0.3em 0.1em 0.3em;
|
padding : 0.1em 0.3em 0.1em 0.3em;
|
||||||
|
|
|
@ -18,6 +18,7 @@ except ImportError:
|
||||||
CustomizationPolicy = None
|
CustomizationPolicy = None
|
||||||
from OFS.Image import File
|
from OFS.Image import File
|
||||||
from ZPublisher.HTTPRequest import FileUpload
|
from ZPublisher.HTTPRequest import FileUpload
|
||||||
|
from AccessControl import getSecurityManager
|
||||||
from DateTime import DateTime
|
from DateTime import DateTime
|
||||||
from Products.CMFCore import utils as cmfutils
|
from Products.CMFCore import utils as cmfutils
|
||||||
from Products.CMFCore.utils import getToolByName
|
from Products.CMFCore.utils import getToolByName
|
||||||
|
@ -25,6 +26,7 @@ from Products.CMFPlone.PloneBatch import Batch
|
||||||
from Products.CMFPlone.utils import ToolInit
|
from Products.CMFPlone.utils import ToolInit
|
||||||
from Products.CMFCore import DirectoryView
|
from Products.CMFCore import DirectoryView
|
||||||
from Products.CMFCore.DirectoryView import manage_addDirectoryView
|
from Products.CMFCore.DirectoryView import manage_addDirectoryView
|
||||||
|
from Products.DCWorkflow.Transitions import TRIGGER_USER_ACTION
|
||||||
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
from Products.ExternalMethod.ExternalMethod import ExternalMethod
|
||||||
from Products.Archetypes.Extensions.utils import installTypes
|
from Products.Archetypes.Extensions.utils import installTypes
|
||||||
from Products.Archetypes.Extensions.utils import install_subskin
|
from Products.Archetypes.Extensions.utils import install_subskin
|
||||||
|
|
|
@ -33,4 +33,52 @@ def updateRolesForPermission(permission, roles, obj):
|
||||||
existingRoles = perm.getRoles()
|
existingRoles = perm.getRoles()
|
||||||
allRoles = set(existingRoles).union(roles)
|
allRoles = set(existingRoles).union(roles)
|
||||||
obj.manage_permission(permission, tuple(allRoles), acquire=0)
|
obj.manage_permission(permission, tuple(allRoles), acquire=0)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
def checkTransitionGuard(guard, sm, wf_def, ob):
|
||||||
|
'''This method is similar to DCWorkflow.Guard.check, but allows to retrieve
|
||||||
|
the truth value as a appy.gen.No instance, not simply "1" or "0".'''
|
||||||
|
from Products.DCWorkflow.Expression import StateChangeInfo,createExprContext
|
||||||
|
u_roles = None
|
||||||
|
if wf_def.manager_bypass:
|
||||||
|
# Possibly bypass.
|
||||||
|
u_roles = sm.getUser().getRolesInContext(ob)
|
||||||
|
if 'Manager' in u_roles:
|
||||||
|
return 1
|
||||||
|
if guard.permissions:
|
||||||
|
for p in guard.permissions:
|
||||||
|
if _checkPermission(p, ob):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
if guard.roles:
|
||||||
|
# Require at least one of the given roles.
|
||||||
|
if u_roles is None:
|
||||||
|
u_roles = sm.getUser().getRolesInContext(ob)
|
||||||
|
for role in guard.roles:
|
||||||
|
if role in u_roles:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
if guard.groups:
|
||||||
|
# Require at least one of the specified groups.
|
||||||
|
u = sm.getUser()
|
||||||
|
b = aq_base( u )
|
||||||
|
if hasattr( b, 'getGroupsInContext' ):
|
||||||
|
u_groups = u.getGroupsInContext( ob )
|
||||||
|
elif hasattr( b, 'getGroups' ):
|
||||||
|
u_groups = u.getGroups()
|
||||||
|
else:
|
||||||
|
u_groups = ()
|
||||||
|
for group in guard.groups:
|
||||||
|
if group in u_groups:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
expr = guard.expr
|
||||||
|
if expr is not None:
|
||||||
|
econtext = createExprContext(StateChangeInfo(ob, wf_def))
|
||||||
|
res = expr(econtext)
|
||||||
|
return res
|
||||||
|
return 1
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
|
@ -222,11 +222,11 @@ class AbstractWrapper:
|
||||||
will be no trace from this transition triggering in the workflow
|
will be no trace from this transition triggering in the workflow
|
||||||
history.'''
|
history.'''
|
||||||
wfTool = self.o.portal_workflow
|
wfTool = self.o.portal_workflow
|
||||||
availableTransitions = [t['id'] for t in \
|
availableTransitions = [t['id'] for t in self.o.getAppyTransitions(\
|
||||||
wfTool.getTransitionsFor(self.o)]
|
includeFake=False, includeNotShowable=True)]
|
||||||
transitionName = transition
|
transitionName = transition
|
||||||
if not transitionName in availableTransitions:
|
if not transitionName in availableTransitions:
|
||||||
# Maybe is is a compound Appy transition. Try to find the
|
# Maybe is it a compound Appy transition. Try to find the
|
||||||
# corresponding DC transition.
|
# corresponding DC transition.
|
||||||
state = self.state
|
state = self.state
|
||||||
transitionPrefix = transition + state[0].upper() + state[1:] + 'To'
|
transitionPrefix = transition + state[0].upper() + state[1:] + 'To'
|
||||||
|
|
Loading…
Reference in a new issue