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:
Gaetan Delannay 2011-02-01 11:09:54 +01:00
parent b48525c5bb
commit 77112c45be
11 changed files with 141 additions and 34 deletions

View file

@ -2054,6 +2054,13 @@ class Transition:
Else, returns False.'''
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):
'''Returns the fromState(s) if p_fromStates is True, the toState(s)
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.
title = String(multiplicity=(1,1), show='edit')
title.init('title', None, 'appy')
state = String()
state.init('state', None, 'appy')
# ------------------------------------------------------------------------------

View file

@ -24,6 +24,7 @@ class ToolMixin(BaseMixin):
'''Returns the name of the portal_type that is based on
p_metaTypeOrAppyType.'''
appName = self.getProductConfig().PROJECTNAME
res = metaTypeOrAppyClass
if not isinstance(metaTypeOrAppyClass, basestring):
res = getClassName(metaTypeOrAppyClass, appName)
if res.find('Extensions_appyWrappers') != -1:

View file

@ -5,11 +5,11 @@
# ------------------------------------------------------------------------------
import os, os.path, sys, types, mimetypes, urllib
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.layout import Table, defaultPageLayouts
from appy.gen.plone25.descriptors import ClassDescriptor
from appy.gen.plone25.utils import updateRolesForPermission
from appy.gen.plone25.utils import updateRolesForPermission,checkTransitionGuard
# ------------------------------------------------------------------------------
class BaseMixin:
@ -342,6 +342,7 @@ class BaseMixin:
def showField(self, name, layoutType='view'):
'''Must I show field named p_name on this p_layoutType ?'''
if name == 'state': return False
return self.getAppyType(name).isShowable(self, layoutType)
def getMethod(self, methodName):
@ -560,24 +561,66 @@ class BaseMixin:
res.append(StateDescr(stateName, stateStatus).get())
return res
def getAppyTransitions(self):
'''Returns the transitions that the user can trigger on p_self.'''
transitions = self.portal_workflow.getTransitionsFor(self)
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?)'''
res = []
if transitions:
# Retrieve the corresponding Appy transition, to check if the user
# may view it.
workflow = self.getWorkflow(appy=True)
if not workflow: return transitions
for transition in transitions:
# Get the corresponding Appy transition
appyTr = workflow._transitionsMapping[transition['id']]
if self._appy_showTransition(workflow, appyTr.show):
transition['confirm'] = ''
if appyTr.confirm:
label = '%s_confirm' % transition['name']
transition['confirm']=self.translate(label, format='js')
res.append(transition)
# 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)
# 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:
label = '%s_confirm' % tInfo['name']
tInfo['confirm'] = self.translate(label, format='js')
if not mayTrigger:
tInfo['may_trigger'] = False
tInfo['reason'] = mayTrigger.msg
res.append(tInfo)
return res
def getAppyPhases(self, currentOnly=False, layoutType='view'):
@ -947,12 +990,6 @@ class BaseMixin:
return stateShow(workflow, self.appy())
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):
'''When an object is created or updated, we must update "add"
permissions accordingly: if the object is a folder, we must set on

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 B

View file

@ -528,9 +528,15 @@
<tal:comment replace="nothing">Buttons for triggering transitions</tal:comment>
<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']);
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>
</tr>
</table>

View file

@ -34,10 +34,9 @@
</tal:publishedObject>
<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>
<dt tal:condition="python: tool.userMaySearch(rootClass)"
tal:attributes="class python:test((repeat['rootClass'].number()==1) and not contextObj, 'portletAppyItem', 'portletAppyItem portletSep')">
<dt 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">
<tr>
<td>

View file

@ -91,12 +91,12 @@
<tal:comment replace="nothing">Workflow state</tal:comment>
<td id="field_workflow_state"
tal:condition="python: widget['name'] == 'workflowState'"
tal:condition="python: widget['name'] == 'state'"
tal:content="python: tool.translate(obj.getWorkflowLabel())">
</td>
<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:field define="contextObj python:obj;
layoutType python:'cell';

View file

@ -180,6 +180,11 @@ th {
/* 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 td, .stx table td {
padding : 0.1em 0.3em 0.1em 0.3em;

View file

@ -18,6 +18,7 @@ except ImportError:
CustomizationPolicy = None
from OFS.Image import File
from ZPublisher.HTTPRequest import FileUpload
from AccessControl import getSecurityManager
from DateTime import DateTime
from Products.CMFCore import utils as cmfutils
from Products.CMFCore.utils import getToolByName
@ -25,6 +26,7 @@ from Products.CMFPlone.PloneBatch import Batch
from Products.CMFPlone.utils import ToolInit
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

View file

@ -33,4 +33,52 @@ def updateRolesForPermission(permission, roles, obj):
existingRoles = perm.getRoles()
allRoles = set(existingRoles).union(roles)
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
# ------------------------------------------------------------------------------

View file

@ -222,11 +222,11 @@ class AbstractWrapper:
will be no trace from this transition triggering in the workflow
history.'''
wfTool = self.o.portal_workflow
availableTransitions = [t['id'] for t in \
wfTool.getTransitionsFor(self.o)]
availableTransitions = [t['id'] for t in self.o.getAppyTransitions(\
includeFake=False, includeNotShowable=True)]
transitionName = transition
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.
state = self.state
transitionPrefix = transition + state[0].upper() + state[1:] + 'To'