From a14bff45a7e84b12c6a8889133858adfbc6f9229 Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Wed, 23 Jul 2014 22:29:47 +0200 Subject: [PATCH] [gen] More work on Ref.link='popup'. --- fields/ref.py | 108 ++++++++++++++++++++++++++---------- gen/mixins/ToolMixin.py | 6 +- gen/ui/appy.js | 77 +++++++++++++++++++++---- gen/wrappers/ToolWrapper.py | 52 +++++++++++------ 4 files changed, 183 insertions(+), 60 deletions(-) diff --git a/fields/ref.py b/fields/ref.py index 07ebfa6..3a139c6 100644 --- a/fields/ref.py +++ b/fields/ref.py @@ -15,7 +15,7 @@ # Appy. If not, see . # ------------------------------------------------------------------------------ -import sys, re +import sys, re, os.path from appy import Object from appy.fields import Field from appy.fields.search import Search @@ -433,33 +433,48 @@ class Ref(Field): ''') pxCell = pxView - pxEdit = Px(''' - - - + + +
-
+ +
- -
''') + href=":'%s/query?className=%s&search=%s:%s&popup=1' % \ + (ztool.absolute_url(), tiedClassName, obj.uid, field.name)"> + + ''') + + pxEdit = Px(''' + + + :field.pxEditPopup''') pxSearch = Px(''' @@ -1243,6 +1258,42 @@ class Ref(Field): if (layoutType == 'view') and (self.render == 'menus'): return 'list' return self.render + def getPopupObjects(self, obj, rq, requestValue): + '''Gets the list of objects that were selected in the popup (for Ref + fields with link="popup").''' + if requestValue: + # We are validating the form. Return the request value instead of + # the popup value. + tool = obj.tool + if isinstance(requestValue, basestring): + return [tool.getObject(requestValue)] + else: + return [tool.getObject(rv) for rv in requestValue] + res = [] + # No object can be selected if the popup has not been opened yet. + if 'semantics' not in rq: + # In this case, display already linked objects if any. + if not obj.isEmpty(self.name): return getattr(obj, self.name) + return res + uids = rq['selected'].split(',') + tool = obj.tool + if rq['semantics'] == 'checked': + # Simply get the selected objects from their uid. + return [tool.getObject(uid) for uid in uids] + else: + # Replay the search in self.select to get the list of uids that were + # shown in the popup. + className = tool.o.getPortalType(self.klass) + brains = obj.o.executeQuery(className, search=self.select, + maxResults='NO_LIMIT', brainsOnly=True, + sortBy=rq.get('sortKey'), sortOrder=rq.get('sortOrder'), + filterKey=rq.get('filterKey'),filterValue=rq.get('filterValue')) + queryUids = [os.path.basename(b.getPath()) for b in brains] + for uid in queryUids: + if uid not in uids: + res.append(tool.getObject(uid)) + return res + def onUiRequest(self, obj, rq): '''This method is called when an action tied to this Ref field is triggered from the user interface (link, unlink, link_many, @@ -1261,8 +1312,7 @@ class Ref(Field): # operation. obj.mayEdit(self.writePermission, raiseError=True) # Get the (un-)checked objects from the request. - uids = rq['targetUid'].strip(',') or () - if uids: uids = uids.split(',') + uids = rq['targetUid'].split(',') unchecked = rq['semantics'] == 'unchecked' if action == 'link_many': # Get possible values (objects) diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 0ea986b..34f1a09 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -356,7 +356,7 @@ class ToolMixin(BaseMixin): brains = self.getPath("/catalog")(**params) if brainsOnly: # Return brains only. - if not maxResults: return brains + if not maxResults or (maxResults == 'NO_LIMIT'): return brains else: return brains[:maxResults] if not maxResults: if refField: maxResults = refField.maxPerPage @@ -731,8 +731,8 @@ class ToolMixin(BaseMixin): res = Search('customSearch', **fields) elif ':' in name: # The search is defined in a Ref field with link=popup - refClass, ref = name.split(':') - res = getattr(self.getAppyClass(refClass), ref).select + refObject, ref = name.split(':') + res = getattr(self.getObject(refObject,appy=True).klass,ref).select elif name: appyClass = self.getAppyClass(className) # Search among static searches diff --git a/gen/ui/appy.js b/gen/ui/appy.js index 308d0bf..f5b5f30 100644 --- a/gen/ui/appy.js +++ b/gen/ui/appy.js @@ -105,6 +105,20 @@ function injectChunk(elem, content){ } } +function getAjaxHook(hookId) { + /* Gets the XHTML element whose ID is p_hookId: it will be the placeholder + for the result of an ajax request. If p_hookId starts with ':', we search + the element in the top browser window, not in the current one that can be + an iframe.*/ + var container = window.document; + var startIndex = 0; + if (hookId[0] == ':') { + container = window.top.document; + startIndex = 1; + } + return container.getElementById(hookId.slice(startIndex)); +} + function getAjaxChunk(pos) { // This function is the callback called by the AJAX machinery (see function // askAjaxChunk below) when an Ajax response is available. @@ -114,13 +128,13 @@ function getAjaxChunk(pos) { var hook = xhrObjects[pos].hook; if (xhrObjects[pos].xhr.readyState == 1) { // The request has been initialized: display the waiting radar - var hookElem = document.getElementById(hook); + var hookElem = getAjaxHook(hook); if (hookElem) injectChunk(hookElem, "
<\/div>"); } if (xhrObjects[pos].xhr.readyState == 4) { // We have received the HTML chunk - var hookElem = document.getElementById(hook); + var hookElem = getAjaxHook(hook); if (hookElem && (xhrObjects[pos].xhr.status == 200)) { injectChunk(hookElem, xhrObjects[pos].xhr.responseText); // Call a custom Javascript function if required @@ -148,7 +162,8 @@ function askAjaxChunk(hook,mode,url,px,params,beforeSend,onGet) { directly on the object at p_url. p_hook is the ID of the XHTML element that will be filled with the XHTML - result from the server. + result from the server. If it starts with ':', we will find the element in + the top browser window and not in the current one (that can be an iframe). p_beforeSend is a Javascript function to call before sending the request. This function will get 2 args: the XMLHttpRequest object and the p_params. @@ -264,10 +279,11 @@ function askRefField(hookId, objectUrl, fieldName, innerRef, startNumber, } function askField(hookId, objectUrl, layoutType, showChanges, masterValues, - requestValue, error, className){ + requestValue, error, className, customParams){ // Sends an Ajax request for getting the content of any field. var fieldName = hookId.split('_')[1]; var params = {'layoutType': layoutType, 'showChanges': showChanges}; + if (customParams){for (var key in customParams) params[key]=customParams[key]} if (masterValues) params['masterValues'] = masterValues.join('*'); if (requestValue) params[fieldName] = requestValue; if (error) params[fieldName + '_error'] = error; @@ -575,17 +591,23 @@ function onLink(action, sourceUid, fieldName, targetUid) { f.submit(); } +function stringFromDictKeys(d){ + // Gets a string containing comma-separated keys from dict p_d. + var res = []; + for (var key in d) res.push(key); + return res.join(); +} + function onLinkMany(action, id) { var elems = _rsplit(id, '_', 3); // Get the DOM node corresponding to the Ref field. var node = document.getElementById(elems[0] + '_' + elems[1]); // Get the uids of (un-)checked objects. var statuses = node['_appy_' + elems[2] + '_cbs']; - var uids = ''; - for (var uid in statuses) uids += uid + ','; + var uids = stringFromDictKeys(statuses); // Get the array semantics var semantics = node['_appy_' + elems[2] + '_sem']; - // Show an error messagge if non element is selected. + // Show an error message if no element is selected. if ((semantics == 'checked') && (len(statuses) == 0)) { openPopup('alertPopup', no_elem_selected); return; @@ -674,9 +696,7 @@ function generatePod(uid, fieldName, template, podFormat, queryData, // We must collect selected objects from a Ref field. var node = document.getElementById(uid + '_' + getChecked); if (node && node.hasOwnProperty('_appy_objs_cbs')) { - var uids = []; - for (var uid in node['_appy_objs_cbs']) uids.push(uid); - f.checkedUids.value = uids.join(); + f.checkedUids.value = stringFromDictKeys(node['_appy_objs_cbs']); f.checkedSem.value = node['_appy_objs_sem']; } } @@ -749,9 +769,12 @@ function openPopup(popupId, msg, width, height) { } function closePopup(popupId) { - // Close the popup - var container = window.parent.document; + // Get the popup + var container = null; + if (popupId == 'iframePopup') container = window.parent.document; + else container = window.document; var popup = container.getElementById(popupId); + // Close the popup popup.style.display = 'none'; popup.style.width = null; if (popupId == 'iframePopup') { @@ -1004,3 +1027,33 @@ function onSelectDate(cal) { cal.callCloseHandler(); } } + +function onSelectObjects(nodeId, objectUrl, sortKey, sortOrder, + filterKey, filterValue){ + /* Objects have been selected in a popup, to be linked via a Ref with + link='popup'. Get them. */ + var node = document.getElementById(nodeId); + var uids = stringFromDictKeys(node['_appy_objs_cbs']); + var semantics = node['_appy_objs_sem']; + // Show an error message if no element is selected. + if ((semantics == 'checked') && (!uids)) { + openPopup('alertPopup', no_elem_selected); + return; + } + // Close the popup. + var parent = window.parent; + closePopup('iframePopup'); + /* Refresh the Ref edit widget to include the linked objects. All those + parameters are needed to replay the query in the popup. */ + askField(':'+nodeId, objectUrl, 'edit', null, null, null, null, null, + {'selected': uids, 'semantics': semantics, 'sortKey': sortKey, + 'sortOrder': sortOrder, 'filterKey': filterKey, + 'filterValue': filterValue}); +} + +// Sets the focus on the correct element in some page. +function initFocus(pageId){ + var id = pageId + '_title'; + var elem = document.getElementById(id); + if (elem) elem.focus(); +} diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 80a8077..1bebe36 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -31,12 +31,17 @@ class ToolWrapper(AbstractWrapper): (q(field.name), q('desc'), q(filterKey)))" src=":url('sortUp.gif')" class="clickable"/> - - - + + + + ''') # Buttons for navigating among a list of objects (from a Ref field or a @@ -323,7 +328,7 @@ class ToolWrapper(AbstractWrapper): name="subTitle">::zobj.getSubTitle() - +
+ + +
@@ -383,6 +388,9 @@ class ToolWrapper(AbstractWrapper):
:_('query_no_result')
+ +
+ +
-
''') + + ''') # Show query results as a grid. pxQueryResultGrid = Px(''' @@ -426,7 +446,7 @@ class ToolWrapper(AbstractWrapper): _=ztool.translate; className=req['className']; searchName=req.get('search', ''); - rootHookId=rootHookId|'%s_search' % className; + rootHookId=rootHookId|searchName.replace(':', '_'); uiSearch=uiSearch|ztool.getSearch(className,searchName,ui=True); refInfo=ztool.getRefInfo(); refObject=refInfo[0]; @@ -452,7 +472,7 @@ class ToolWrapper(AbstractWrapper): (q(ajaxHookId), q(ztool.absolute_url()), q(className), \ q(searchName),int(inPopup)); showNewSearch=showNewSearch|True; - enableLinks=enableLinks|True; + enableLinks=enableLinks|not inPopup; newSearchUrl='%s/search?className=%s%s' % \ (ztool.absolute_url(), className, refUrlPart); showSubTitles=req.get('showSubTitles', 'true') == 'true'; @@ -460,7 +480,7 @@ class ToolWrapper(AbstractWrapper): target=ztool.getLinksTargetInfo(ztool.getAppyClass(className))" id=":ajaxHookId"> - + :tool.pxNavigate - + :_('query_no_result')
:_('search_new')
@@ -513,12 +533,12 @@ class ToolWrapper(AbstractWrapper):
- :tool.pxPagePrologue:tool.pxQueryResult + :tool.pxPagePrologue:tool.pxQueryResult
''', template=AbstractWrapper.pxTemplate, hook='content') pxSearch = Px('''