From f0c1f69573232a0d9f0ee426c9eed6704514821a Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Mon, 21 Apr 2014 12:11:41 +0200 Subject: [PATCH] [gen] Optimisations, bugfixes and refactorings. --- fields/ref.py | 3 +- fields/workflow.py | 34 ++++++--- gen/mixins/__init__.py | 148 +++++++++++++++--------------------- gen/ui/appy.css | 4 +- gen/ui/appy.js | 6 +- gen/ui/search.png | Bin 0 -> 297 bytes gen/utils.py | 2 +- gen/wrappers/ToolWrapper.py | 2 +- gen/wrappers/__init__.py | 20 +++-- 9 files changed, 104 insertions(+), 115 deletions(-) create mode 100644 gen/ui/search.png diff --git a/fields/ref.py b/fields/ref.py index bb46894..dd9307f 100644 --- a/fields/ref.py +++ b/fields/ref.py @@ -270,8 +270,7 @@ class Ref(Field): :field.pxRender + if="field.isShowable(zobj, 'result')">:field.pxRender diff --git a/fields/workflow.py b/fields/workflow.py index ce9922d..4de2e3c 100644 --- a/fields/workflow.py +++ b/fields/workflow.py @@ -331,7 +331,7 @@ class Transition: return msg def trigger(self, transitionName, obj, wf, comment, doAction=True, - doNotify=True, doHistory=True, doSay=True): + doNotify=True, doHistory=True, doSay=True, reindex=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 @@ -341,7 +341,9 @@ class Transition: 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.''' + trigger programmatically, and no message is returned to the user. + If p_reindex is False, object reindexing will be performed by the + calling method.''' # Create the workflow_history dict if it does not exist. if not hasattr(obj.aq_base, 'workflow_history'): from persistent.mapping import PersistentMapping @@ -368,12 +370,12 @@ class Transition: if not doHistory: comment = '_invisible_' obj.addHistoryEvent(action, review_state=targetStateName, comments=comment) - # Reindex the object if required. Not only security-related indexes - # (Allowed, State) need to be updated here. - if not obj.isTemporary(): obj.reindex() # Execute the related action if needed msg = '' if doAction and self.action: msg = self.executeAction(obj, wf) + # Reindex the object if required. Not only security-related indexes + # (Allowed, State) need to be updated here. + if reindex and not obj.isTemporary(): obj.reindex() # Send notifications if needed if doNotify and self.notify and obj.getTool(True).mailEnabled: sendNotification(obj.appy(), self, transitionName, wf) @@ -382,10 +384,21 @@ class Transition: if not msg: msg = obj.translate('object_saved') obj.say(msg) + def onUiRequest(self, obj, wf, name, rq): + '''Executed when a user wants to trigger this transition from the UI.''' + tool = obj.getTool() + # Is this transition triggerable? + if not self.isTriggerable(obj, wf): + raise Exception('Transition "%s" can\'t be triggered.' % name) + # Trigger the transition + self.trigger(name, obj, wf, rq.get('comment', ''), reindex=False) + # Reindex obj if required. + if not obj.isTemporary(): obj.reindex() + return tool.goto(obj.getUrl(rq['HTTP_REFERER'])) + class UiTransition: '''Represents a widget that displays a transition.''' - - pxView = Px(''' + pxView = Px(''' - ''') + value=":label" title=":transition.reason"/>''') def __init__(self, name, transition, obj, mayTrigger, ): self.name = name @@ -419,7 +431,7 @@ class UiTransition: if not mayTrigger: self.mayTrigger = False self.reason = mayTrigger.msg - # Require by the UiGroup. + # Required by the UiGroup. self.colspan = 1 # ------------------------------------------------------------------------------ diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 90d2aec..8752910 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -663,16 +663,6 @@ class BaseMixin: self.REQUEST.set(field.name, '') return self.edit() - def showField(self, name, layoutType='view'): - '''Must I show field named p_name on this p_layoutType ?''' - return self.getAppyType(name).isShowable(self, layoutType) - - def getMethod(self, methodName): - '''Returns the method named p_methodName.''' - # If I write "self.aq_base" instead of self, acquisition will be - # broken on returned object. - return getattr(self, methodName, None) - def getCreateFolder(self): '''When an object must be created from this one through a Ref field, we must know where to put the newly create object: within this one if it @@ -847,55 +837,6 @@ class BaseMixin: return klass.styles[elem] return elem - def getTransitions(self, includeFake=True, includeNotShowable=False, - grouped=True): - '''This method returns info about transitions (as UiTransition - instances) 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 UI 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. - * If p_grouped is True, transitions are grouped according to their - "group" attribute, in a similar way to fields or searches. - ''' - res = [] - groups = {} # The already encountered groups of transitions. - wfPage = gen.Page('workflow') - wf = self.getWorkflow() - currentState = self.State(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: - includeIt = mayTrigger - else: - includeIt = mayTrigger or isinstance(mayTrigger, gen.No) - if not includeNotShowable: - includeIt = includeIt and transition.isShowable(wf, self) - if not includeIt: continue - # Create the UiTransition instance. - info = UiTransition(name, transition, self, mayTrigger) - # Add the transition into the result. - if not transition.group or not grouped: - res.append(info) - else: - # Insert the UiGroup instance corresponding to transition.group. - uiGroup = transition.group.insertInto(res, groups, wfPage, - self.__class__.__name__, content='transitions') - uiGroup.addElement(info) - return res - def getAppyPhases(self, currentOnly=False, layoutType='view'): '''Gets the list of phases that are defined for this content type. If p_currentOnly is True, the search is limited to the phase where the @@ -965,10 +906,10 @@ class BaseMixin: if hasattr(appyObj, 'getSubTitle'): return appyObj.getSubTitle() return '' - def notifyWorkflowCreated(self): - '''This method is called every time an object is created, be it temp or - not. The objective here is to initialise workflow-related data on - the object.''' + # Workflow methods ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + def initializeWorkflow(self): + '''Called when an object is created, be it temp or not, for initializing + workflow-related data on the object.''' wf = self.getWorkflow() # Get the initial workflow state initialState = self.State(name=False) @@ -994,6 +935,55 @@ class BaseMixin: stateName = stateName or self.State() return '%s_%s' % (self.getWorkflow(name=True), stateName) + def getTransitions(self, includeFake=True, includeNotShowable=False, + grouped=True): + '''This method returns info about transitions (as UiTransition + instances) 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 UI 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. + * If p_grouped is True, transitions are grouped according to their + "group" attribute, in a similar way to fields or searches. + ''' + res = [] + groups = {} # The already encountered groups of transitions. + wfPage = gen.Page('workflow') + wf = self.getWorkflow() + currentState = self.State(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: + includeIt = mayTrigger + else: + includeIt = mayTrigger or isinstance(mayTrigger, gen.No) + if not includeNotShowable: + includeIt = includeIt and transition.isShowable(wf, self) + if not includeIt: continue + # Create the UiTransition instance. + info = UiTransition(name, transition, self, mayTrigger) + # Add the transition into the result. + if not transition.group or not grouped: + res.append(info) + else: + # Insert the UiGroup instance corresponding to transition.group. + uiGroup = transition.group.insertInto(res, groups, wfPage, + self.__class__.__name__, content='transitions') + uiGroup.addElement(info) + return res + def applyUserIdChange(self, oldId, newId): '''A user whose ID was p_oldId has now p_newId. If the old ID was mentioned in self's local roles, update it to the new ID. This @@ -1156,34 +1146,20 @@ class BaseMixin: return True def onExecuteAction(self): - '''This method is called every time a user wants to execute an Appy - action on an object.''' + '''Called when a user wants to execute an Appy action on an object.''' rq = self.REQUEST return self.getAppyType(rq['fieldName']).onUiRequest(self, rq) - def trigger(self, transitionName, comment='', doAction=True, doNotify=True, - doHistory=True, doSay=True, noSecurity=False): - '''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, noSecurity=noSecurity): - 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 onTrigger(self): - '''This method is called whenever a user wants to trigger a workflow - transition on an object.''' + '''Called when a user wants to trigger a transition on an object.''' rq = self.REQUEST - self.trigger(rq['workflow_action'], comment=rq.get('comment', '')) - self.reindex() - return self.goto(self.getUrl(rq['HTTP_REFERER'])) + wf = self.getWorkflow() + # Get the transition + name = rq['transition'] + transition = getattr(wf, name, None) + if not transition or (transition.__class__.__name__ != 'Transition'): + raise Exception('Transition "%s" not found.' % name) + return transition.onUiRequest(self, wf, name, rq) def getRolesFor(self, permission): '''Gets, according to the workflow, the roles that are currently granted diff --git a/gen/ui/appy.css b/gen/ui/appy.css index 7cdd033..dca0bed 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -70,7 +70,7 @@ input.button { color: #666666; height: 20px; width: 130px; cursor:pointer; font-size: 90%; padding-left: 10px; background-color: white; background-repeat: no-repeat; background-position: 5% 25%; box-shadow: 2px 2px 2px #888888} -.fakeButton { background-color: #e6e6e6 !important ; cursor:help !important } +.fake { background-color: #e6e6e6 !important ; cursor:help !important } .buttons { margin-left: 4px } .message { position: absolute; top: -40px; left: 50%; font-size: 90%; width: 600px; border: 1px #F0C36D solid; padding: 6px; @@ -99,7 +99,7 @@ td.search { padding-top: 8px } .content { padding: 14px 14px 9px 15px; background-color: #f1f1f1 } .popup { display: none; position: absolute; top: 30%; left: 35%; width: 350px; z-index : 100; background: white; padding: 8px; - border: 1px solid grey } + border: 1px solid grey; box-shadow: 2px 2px 2px #888888} .dropdown { display:none; position: absolute; border: 1px solid #cccccc; background-color: white; padding: 3px 4px 0 } .dropdownMenu { cursor: pointer; padding-right: 4px; font-size: 93% } diff --git a/gen/ui/appy.js b/gen/ui/appy.js index d2b5c4b..9031f3b 100644 --- a/gen/ui/appy.js +++ b/gen/ui/appy.js @@ -529,10 +529,8 @@ function submitAppyForm(button) { // Function used for triggering a workflow transition function triggerTransition(formId, transitionId, msg) { var theForm = document.getElementById(formId); - theForm.workflow_action.value = transitionId; - if (!msg) { - theForm.submit(); - } + theForm.transition.value = transitionId; + if (!msg) { theForm.submit(); } else { // Ask the user to confirm. askConfirm('form', formId, msg, true); } diff --git a/gen/ui/search.png b/gen/ui/search.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1dd41b369a159f2adf343f1a7782256cb434bf GIT binary patch literal 297 zcmeAS@N?(olHy`uVBq!ia0vp^{6H+g!3-q-bzC(AQjEnx?oJHr&dIz4a{K~(LR_Cd zeR}NJv8z|FE?&I&Qxzm=iPSjtvK6nqHiqU0qp9C1Xk@TbiR{Vws={^Ua$#U2nJs mz7-Kx*HLIxQ#k2h#K2&h#Z=xaC|L_MlEKr}&t;ucLK6UX&U8co literal 0 HcmV?d00001 diff --git a/gen/utils.py b/gen/utils.py index 0454714..890064d 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -46,7 +46,7 @@ def createObject(folder, id, className, appName, wf=True, noSecurity=False): obj.modified = obj.created from persistent.mapping import PersistentMapping obj.__ac_local_roles__ = PersistentMapping({ userId: ['Owner'] }) - if wf: obj.notifyWorkflowCreated() + if wf: obj.initializeWorkflow() return obj # ------------------------------------------------------------------------------ diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 1cb057a..0302a2e 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -347,7 +347,7 @@ class ToolWrapper(AbstractWrapper): :field.pxRender + if="field.isShowable(zobj, 'result')">:field.pxRender ''') # Show query results as a list. diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index a8b1889..78eb486 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -366,7 +366,7 @@ class AbstractWrapper(object): var2="formId='trigger_%s' % targetObj.UID()" method="post" id=":formId" action=":targetObj.absolute_url() + '/do'"> - + @@ -854,13 +854,17 @@ class AbstractWrapper(object): return self.o.translate(label, mapping, domain, language=language, format=format) - def do(self, transition, comment='', doAction=True, doNotify=True, - doHistory=True, noSecurity=False): - '''This method allows to trigger on p_self a workflow p_transition - programmatically. See doc in self.o.do.''' - return self.o.trigger(transition, comment, doAction=doAction, - doNotify=doNotify, doHistory=doHistory, - doSay=False, noSecurity=noSecurity) + def do(self, name, comment='', doAction=True, doNotify=True, doHistory=True, + noSecurity=False): + '''Triggers on p_self a transition named p_name programmatically.''' + o = self.o + wf = o.getWorkflow() + tr = getattr(wf, name, None) + if not tr or (tr.__class__.__name__ != 'Transition'): + raise Exception('Transition "%s" not found.' % name) + return tr.trigger(name, o, wf, comment, doAction=doAction, + doNotify=doNotify, doHistory=doHistory, doSay=False, + noSecurity=noSecurity) def log(self, message, type='info'): return self.o.log(message, type) def say(self, message, type='info'): return self.o.say(message, type)