From 77112c45be86314c074e34027f72d85c5a67c34a Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Tue, 1 Feb 2011 11:09:54 +0100 Subject: [PATCH] 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. --- gen/__init__.py | 9 +++ gen/plone25/mixins/ToolMixin.py | 1 + gen/plone25/mixins/__init__.py | 87 ++++++++++++++++++-------- gen/plone25/skin/fakeTransition.gif | Bin 0 -> 62 bytes gen/plone25/skin/page.pt | 8 ++- gen/plone25/skin/portlet.pt | 5 +- gen/plone25/skin/result.pt | 4 +- gen/plone25/templates/Styles.css.dtml | 5 ++ gen/plone25/templates/config.py | 2 + gen/plone25/utils.py | 48 ++++++++++++++ gen/plone25/wrappers/__init__.py | 6 +- 11 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 gen/plone25/skin/fakeTransition.gif diff --git a/gen/__init__.py b/gen/__init__.py index ae9a670..9189582 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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') # ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index f9e8d4f..832c724 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -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: diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 546fd1b..93e490b 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -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 diff --git a/gen/plone25/skin/fakeTransition.gif b/gen/plone25/skin/fakeTransition.gif new file mode 100644 index 0000000000000000000000000000000000000000..ed2f607616572515dd2d0a4ce2889feca9047212 GIT binary patch literal 62 zcmZ?wbhEHbWMklESjfb1UPJs#fc<|kQ2fcl$iTqFpu+$JAbAER{we+J4Y$`UC^Wn& L!gT7o2!k~MY$g+} literal 0 HcmV?d00001 diff --git a/gen/plone25/skin/page.pt b/gen/plone25/skin/page.pt index d9189f2..6b99f40 100644 --- a/gen/plone25/skin/page.pt +++ b/gen/plone25/skin/page.pt @@ -528,9 +528,15 @@ Buttons for triggering transitions - Real button + + Fake button, explaining why the transition can't be triggered +
+ +
diff --git a/gen/plone25/skin/portlet.pt b/gen/plone25/skin/portlet.pt index d7a48af..d945b61 100644 --- a/gen/plone25/skin/portlet.pt +++ b/gen/plone25/skin/portlet.pt @@ -34,10 +34,9 @@ Create a section for every root class. - + Section title, with action icons -
+
Any other field -
diff --git a/gen/plone25/skin/result.pt b/gen/plone25/skin/result.pt index 96ce1a1..2b517c9 100644 --- a/gen/plone25/skin/result.pt +++ b/gen/plone25/skin/result.pt @@ -91,12 +91,12 @@ Workflow state