From 1bd6cf29a3648f7ef76a2aa5b950c92441fe6714 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Tue, 3 Feb 2015 10:56:15 +0100 Subject: [PATCH] [gen] Modified Ajax system to be able to ajax-refresh a single row within query results or ref tied object (ongoing work). --- fields/__init__.py | 58 +++++++++ fields/ref.py | 7 +- fields/search.py | 203 +++++++++++++++++++++++++++-- fields/workflow.py | 35 ++--- gen/mixins/ToolMixin.py | 20 ++- gen/mixins/__init__.py | 4 +- gen/ui/appy.js | 84 ++++++++++-- gen/wrappers/ToolWrapper.py | 251 ++---------------------------------- gen/wrappers/__init__.py | 51 ++++++-- px/__init__.py | 4 +- shared/utils.py | 2 + 11 files changed, 417 insertions(+), 302 deletions(-) diff --git a/fields/__init__.py b/fields/__init__.py index 0d7b27d..1602b41 100644 --- a/fields/__init__.py +++ b/fields/__init__.py @@ -97,6 +97,64 @@ class Field: context[k] = ctx[k] return self.pxRender(context).encode('utf-8') + # Show the field content for some object on a list of results + pxRenderAsResult = Px(''' + + + + ::sup + ::zobj.getListTitle(mode=titleMode, nav=navInfo, target=target, \ + page=pageName, inPopup=inPopup, selectJs=selectJs, highlight=True) + ::zobj.highlight(sub) + + +
+ + + + + + + + :targetObj.appy().pxTransitions + + + + :field.pxCell + +
+
+ + + :_('unauthorized') + +
+ + + :field.pxRender + ''') + # Displays a field label pxLabel = Px('''''') diff --git a/fields/ref.py b/fields/ref.py index 86529b0..736ccfb 100644 --- a/fields/ref.py +++ b/fields/ref.py @@ -80,7 +80,7 @@ class Ref(Field): # (edit, delete, etc). pxObjectActions = Px('''
+ style=":'display:%s' % field.showActions" var2="layoutType='buttons'"> :tied.pxTransitions + var2="targetObj=tied.o">:tied.pxTransitions + mayView=tied.o.mayView()" + id=":'%s_%s' % (ajaxHookId, tiedUid)"> :field.pxNumber :search.translated
''') + # Search results, as a list (used by pxResult below) + pxResultList = Px(''' + + + + + + + + + + + + + + :obj.pxViewAsResult +
+ + + ::ztool.truncateText(_(field.labelId)) + :tool.pxSortAndFilter + :tool.pxShowDetails +
:_('query_no_result')
+ +
+ +
+ + +
''') + + # Search results, as a grid (used by pxResult below) + pxResultGrid = Px(''' + + + + +
+ :field.pxRenderAsResult +
''') + + # Render search results + pxResult = Px(''' +
+ + + + + + + +
:field.pxRender
+ + +

+ ::uiSearch.translated (:totalNumber) +  — +  :_('search_new') + +

+ + + + + + + +
+ :uiSearch.translatedDescr
+
:tool.pxNavigate
+ + + + :uiSearch.pxResultList + :uiSearch.pxResultGrid + + + + :tool.pxNavigate +
+ + + :_('query_no_result') +
+ :_('search_new')
+
+
''') + def __init__(self, search, className, tool): self.search = search self.name = search.name self.type = 'search' self.colspan = search.colspan + self.className = className # Property "display" of the div tag containing actions for every search # result. self.showActions = search.showActions @@ -188,7 +338,7 @@ class UiSearch: self.translated = search.translated self.translatedDescr = search.translatedDescr else: - # The label may be specific in some special cases. + # The label may be specific in some special cases labelDescr = '' if search.name == 'allSearch': label = '%s_plural' % className @@ -221,6 +371,11 @@ class UiSearch: Else, simply return the name of the search.''' return getattr(self, 'initiatorHook', self.name) + def getResultMode(self, klass): + '''Must we show, on pxResult, instances of p_klass as a list or + as a grid?''' + return getattr(klass, 'resultMode', 'list') + def showCheckboxes(self): '''If checkboxes are enabled for this search (and if an initiator field is there), they must be visible only if the initiator field is @@ -229,4 +384,32 @@ class UiSearch: the DOM because they store object UIDs.''' if not self.search.checkboxes: return return not self.initiator or self.initiatorField.isMultiValued() + + def getCbJsInit(self, hookId): + '''Returns the code that creates JS data structures for storing the + status of checkboxes for every result of this search.''' + default = self.search.checkboxesDefault and 'unchecked' or 'checked' + return '''var node=document.getElementById('%s'); + node['_appy_objs_cbs'] = {}; + node['_appy_objs_sem'] = '%s';''' % (hookId, default) + + def getAjaxData(self, hook, ztool, **params): + '''Initializes an AjaxData object on the DOM node corresponding to + p_hook = the whole search result.''' + # Complete params with default parameters + params['className'] = self.className + params['searchName'] = self.name + params = sutils.getStringDict(params) + return "document.getElementById('%s')['ajax']=new AjaxData('%s', " \ + "'pxResult', %s, null, '%s')" % \ + (hook, hook, params, ztool.absolute_url()) + + def getAjaxDataRow(self, zobj, parentHook, **params): + '''Initializes an AjaxData object on the DOM node corresponding to + p_hook = a row within the list of results.''' + hook = zobj.id + return "document.getElementById('%s')['ajax']=new AjaxData('%s', " \ + "'pxViewAsResultFromAjax',%s,'%s','%s')" % \ + (hook, hook, sutils.getStringDict(params), parentHook, + zobj.absolute_url()) # ------------------------------------------------------------------------------ diff --git a/fields/workflow.py b/fields/workflow.py index 66e4c6e..c3a5e66 100644 --- a/fields/workflow.py +++ b/fields/workflow.py @@ -361,7 +361,7 @@ class Transition: "onTrigger" on the workflow class by convention. The common action is executed before the transition-specific action (if any).''' obj = obj.appy() - wf = wf.__instance__ # We need the prototypical instance here. + wf = wf.__instance__ # We need the prototypical instance here wf.onTrigger(obj, name, fromState) def trigger(self, name, obj, wf, comment, doAction=True, doHistory=True, @@ -374,11 +374,11 @@ class Transition: the transition is triggered programmatically, and no message is returned to the user. If p_reindex is False, object reindexing will be performed by the calling method.''' - # "Triggerability" and security checks. + # "Triggerability" and security checks if (name != '_init_') and \ not self.isTriggerable(obj, wf, noSecurity=noSecurity): raise Exception('Transition "%s" can\'t be triggered.' % name) - # Create the workflow_history dict if it does not exist. + # Create the workflow_history dict if it does not exist if not hasattr(obj.aq_base, 'workflow_history'): from persistent.mapping import PersistentMapping obj.workflow_history = PersistentMapping() @@ -403,11 +403,11 @@ class Transition: action = None fromState = None else: - fromState = obj.State() # Remember the "from" (=start) state. + fromState = obj.State() # Remember the "from" (=start) state if not doHistory: comment = '_invisible_' obj.addHistoryEvent(action, review_state=targetStateName, comments=comment) - # Execute the action that is common to all transitions, if defined. + # Execute the action that is common to all transitions, if defined if doAction and hasattr(wf, 'onTrigger'): self.executeCommonAction(obj, name, wf, fromState) # Execute the related action if needed @@ -417,19 +417,22 @@ class Transition: # (Allowed, State) need to be updated here. if reindex and not obj.isTemporary(): obj.reindex() # Return a message to the user if needed - if not doSay or (name == '_init_'): return + if not doSay: return if not msg: msg = obj.translate('object_saved') - obj.say(msg) + return msg def onUiRequest(self, obj, wf, name, rq): '''Executed when a user wants to trigger this transition from the UI.''' tool = obj.getTool() # Trigger the transition - self.trigger(name, obj, wf, rq.get('comment', ''), reindex=False) - # Reindex obj if required. + msg = self.trigger(name, obj, wf, rq.get('comment', ''), reindex=False) + # Reindex obj if required if not obj.isTemporary(): obj.reindex() + # If we are called from an Ajax request, return a message + if hasattr(rq, 'pxContext') and rq.pxContext['ajax']: return msg # If we are viewing the object and if the logged user looses the # permission to view it, redirect the user to its home page. + if msg: obj.say(msg) if not obj.mayView() and \ (obj.absolute_url_path() in rq['HTTP_REFERER']): back = tool.getHomePage() @@ -462,22 +465,24 @@ class Transition: if startOk and endOk: return trName class UiTransition: - '''Represents a widget that displays a transition.''' + '''Represents a widget that displays a transition''' pxView = Px(''' + inButtons=layoutType == 'buttons'; + css=ztool.getButtonCss(label, inButtons)"> + onclick=":'triggerTransition(%s,%s,%s,%s)' % (q(formId), \ + q(transition.name), q(transition.confirm), back)"/> ''') - def __init__(self, name, transition, obj, mayTrigger, ): + def __init__(self, name, transition, obj, mayTrigger): self.name = name self.transition = transition self.type = 'transition' @@ -494,7 +499,7 @@ class UiTransition: if not mayTrigger: self.mayTrigger = False self.reason = mayTrigger.msg - # Required by the UiGroup. + # Required by the UiGroup self.colspan = 1 # ------------------------------------------------------------------------------ diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 65c24b2..d423ebe 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -272,12 +272,6 @@ class ToolMixin(BaseMixin): for key in self.queryParamNames]) return res - def getResultMode(self, className): - '''Must we show, on pxQueryResult, instances of p_className as a list or - as a grid?''' - klass = self.getAppyClass(className) - return getattr(klass, 'resultMode', 'list') - def showPortlet(self, obj, layoutType): '''When must the portlet be shown? p_obj and p_layoutType can be None if we are not browing any objet (ie, we are on the home page).''' @@ -1298,4 +1292,18 @@ class ToolMixin(BaseMixin): if field: msg = getattr(field, action)(obj.o) else: msg = getattr(obj.o, action)() return msg + + def updatePxContextFromRequest(self): + '''Takes any user-defined key from the request and put it as a variable + on the current PX context.''' + req = self.REQUEST + ctx = req.pxContext + # Get "form" data (get, post) and cookie values + for source in (req.form, req.cookies): + for k, v in source.iteritems(): + # Convert v to some Python data when relevant + if v in ('True', 'False', 'true', 'false'): + exec 'v = %s' % v.capitalize() + elif v.isdigit(): v = int(v) + ctx[k] = v # ------------------------------------------------------------------------------ diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 40ad75b..91ca102 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -1019,9 +1019,9 @@ class BaseMixin: wf = self.getWorkflow() # Get the initial workflow state initialState = self.State(name=False) - # Create a Transition instance representing the initial transition. + # Create a Transition instance representing the initial transition initialTransition = gen.Transition((initialState, initialState)) - initialTransition.trigger('_init_', self, wf, '') + initialTransition.trigger('_init_', self, wf, '', doSay=False) def getWorkflow(self, name=False, className=None): '''Returns the workflow applicable for p_self (or for any instance of diff --git a/gen/ui/appy.js b/gen/ui/appy.js index f106043..f9ef59f 100644 --- a/gen/ui/appy.js +++ b/gen/ui/appy.js @@ -79,7 +79,7 @@ function XhrObject() { // Wraps a XmlHttpRequest object replaced by result of executing the Ajax request. */ this.onGet = ''; /* The name of a Javascript function to call once we receive the result. */ - this.info = {}; /* An associative array for putting anything else. */ + this.info = {}; /* An associative array for putting anything else */ } /* When inserting HTML at some DOM node in a page via Ajax, scripts defined in @@ -228,11 +228,61 @@ function askAjaxChunk(hook,mode,url,px,params,beforeSend,onGet) { } } +// Object representing all the data required to perform an Ajax request +function AjaxData(hook, px, params, parentHook, url, mode, beforeSend, onGet) { + this.hook = hook; + this.mode = mode; + if (!mode) this.mode = 'GET'; + this.url = url; + this.px = px; + this.params = params; + this.beforeSend = beforeSend; + this.onGet = onGet; + /* If a parentHook is spefified, this AjaxData must be completed with a parent + AjaxData instance. */ + this.parentHook = parentHook; +} + + +function askAjax(hook, form) { + /* Call askAjaxChunk by getting an AjaxData instance from p_hook and a + potential action from p_form). */ + var d = document.getElementById(hook)['ajax']; + // Complete data with a parent data if present + if (d['parentHook']) { + var parent = document.getElementById(d['parentHook'])['ajax']; + for (var key in parent) { + if (key == 'params') continue; // Will get a specific treatment herafter + if (!d[key]) d[key] = parent[key]; // Override if no value on child + } + // Merge parameters + if (parent.params) { + for (var key in parent.params) { + if (key in d.params) continue; // Override if not value on child + d.params[key] = parent.params[key]; + } + } + } + // If a p_form id is given, integrate the form submission in the ajax request + if (form) { + var f = document.getElementById(form); + var mode = 'POST'; + // Deduce the action from the form action + d.params['action'] = _rsplit(f.action, '/', 2)[1]; + // Get the other params + var elems = f.elements; + for (var i=0; i < elems.length; i++) { + d.params[elems[i].name] = elems[i].value; + } + } + else var mode = d.mode; + askAjaxChunk(d.hook,mode,d.url,d.px,d.params,d.beforeSend,d.onGet) } + /* The functions below wrap askAjaxChunk for getting specific content through an Ajax request. */ function askQueryResult(hookId, objectUrl, className, searchName, popup, startNumber, sortKey, sortOrder, filterKey) { - // Sends an Ajax request for getting the result of a query. + // Sends an Ajax request for getting the result of a query var params = {'className': className, 'search': searchName, 'startNumber': startNumber, 'popup': popup}; if (sortKey) params['sortKey'] = sortKey; @@ -244,8 +294,8 @@ function askQueryResult(hookId, objectUrl, className, searchName, popup, params['filterValue'] = encodeURIComponent(filterWidget.value); } } - askAjaxChunk(hookId, 'GET', objectUrl, 'pxQueryResult', params, null, - evalInnerScripts); + var px = className + ':' + searchName + ':' + 'pxResult'; + askAjaxChunk(hookId, 'GET', objectUrl, px, params, null, evalInnerScripts); } function askObjectHistory(hookId, objectUrl, maxPerPage, startNumber) { @@ -265,7 +315,7 @@ function askRefField(hookId, objectUrl, innerRef, startNumber, action, params[startKey] = startNumber; if (action) params['action'] = action; if (actionParams) { - for (key in actionParams) { + for (var key in actionParams) { if ((key == 'move') && (typeof actionParams[key] == 'object')) { // Get the new index from an input field var id = actionParams[key].id; @@ -343,7 +393,7 @@ function _rsplit(s, delimiter, limit) { var elems = s.split(delimiter); var exc = elems.length - limit; if (exc <= 0) return elems; - // Merge back first elements to get p_limit elements. + // Merge back first elements to get p_limit elements var head = ''; var res = []; for (var i=0; i < elems.length; i++) { @@ -582,12 +632,22 @@ function submitAppyForm(button) { } // Function used for triggering a workflow transition -function triggerTransition(formId, transitionId, msg) { - var theForm = document.getElementById(formId); - theForm.transition.value = transitionId; - if (!msg) { theForm.submit(); } - else { // Ask the user to confirm. - askConfirm('form', formId, msg, true); +function triggerTransition(formId, transitionId, msg, back) { + var f = document.getElementById(formId); + f.transition.value = transitionId; + if (!msg) { + /* We must submit the form and either refresh the entire page (back is null) + or ajax-refresh a given part only (p_back corresponds to the id of the + DOM node to be refreshed. */ + if (back) askAjax(back, formId); + else f.submit(); + } + else { + // Ask a confirmation to the user before proceeding + if (back) { + var js = "askAjax('"+back+"', '"+formId+"');" + askConfirm('script', js, msg, true) } + else askConfirm('form', formId, msg, true); } } diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 2541782..6505854 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -100,11 +100,10 @@ class ToolWrapper(AbstractWrapper): - + - +
@@ -271,7 +270,7 @@ class ToolWrapper(AbstractWrapper): ''') - # The message that is shown when a user triggers an action. + # The message that is shown when a user triggers an action pxMessage = Px('''