[gen] More work on Ref.link='popup'.

This commit is contained in:
Gaetan Delannay 2014-07-23 22:29:47 +02:00
parent 792db32f27
commit a14bff45a7
4 changed files with 183 additions and 60 deletions

View file

@ -15,7 +15,7 @@
# Appy. If not, see <http://www.gnu.org/licenses/>. # Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import sys, re import sys, re, os.path
from appy import Object from appy import Object
from appy.fields import Field from appy.fields import Field
from appy.fields.search import Search from appy.fields.search import Search
@ -433,33 +433,48 @@ class Ref(Field):
</x>''') </x>''')
pxCell = pxView pxCell = pxView
pxEdit = Px('''<x if="(field.link) and (field.link != 'list')">
<select if="field.link != 'popup'" # Edit widget, for Refs with link='popup'.
var2="objects=field.getPossibleValues(zobj); pxEditPopup = Px('''
uids=[o.id for o in field.getValue(zobj, appy=False)]" <x var="objects=field.getPopupObjects(obj, req, requestValue)">
name=":name" id=":name" size=":isMultiple and field.height or ''" <!-- The select field allowing to store the selected objects -->
onchange=":field.getOnChange(zobj, layoutType)" <select if="objects" name=":name" id=":name" multiple=":isMultiple"
multiple=":isMultiple"> size=":isMultiple and field.height or ''">
<option value="" if="not isMultiple">:_('choose_a_value')</option> <option for="tied in objects" value=":tied.uid" selected="selected"
<option for="tied in objects" var2="title=field.getReferenceLabel(tied, unlimited=True)"
var2="uid=tied.uid; title=":title">:ztool.truncateValue(title, field.width)</option>
title=field.getReferenceLabel(tied, unlimited=True)" </select>
selected=":inRequest and (uid in requestValue) or \ <div if="not objects">-</div>
(uid in uids)" value=":uid" <!-- The button for opening the popup -->
title=":title">:ztool.truncateValue(title, field.width)</option> <a target="appyIFrame"
</select> var="tiedClassName=ztool.getPortalType(field.klass);
<!-- A button for opening the popup if field.link is "popup". -->
<a if="field.link == 'popup'" target="appyIFrame"
var2="tiedClassName=ztool.getPortalType(field.klass);
className=ztool.getPortalType(obj.klass)" className=ztool.getPortalType(obj.klass)"
href=":'%s/query?className=%s&amp;search=%s:%s&amp;popup=1' % \ href=":'%s/query?className=%s&amp;search=%s:%s&amp;popup=1' % \
(ztool.absolute_url(), tiedClassName, className, field.name)"> (ztool.absolute_url(), tiedClassName, obj.uid, field.name)">
<input type="button" class="buttonSmall button" <input type="button" class="buttonSmall button"
var="label=_('search_button')" value=":label" var="label=_('search_button')" value=":label"
style=":'%s; %s' % (url('search', bg=True), \ style=":'%s; %s' % (url('search', bg=True), \
ztool.getButtonWidth(label))" ztool.getButtonWidth(label))"
onclick="openPopup('iframePopup')"/> onclick="openPopup('iframePopup')"/></a>
</a></x>''') </x>''')
pxEdit = Px('''
<x if="(field.link) and (field.link != 'list')">
<select if="field.link != 'popup'"
var2="objects=field.getPossibleValues(zobj);
uids=[o.id for o in field.getValue(zobj, appy=False)]"
name=":name" id=":name" size=":isMultiple and field.height or ''"
onchange=":field.getOnChange(zobj, layoutType)"
multiple=":isMultiple">
<option value="" if="not isMultiple">:_('choose_a_value')</option>
<option for="tied in objects"
var2="uid=tied.uid;
title=field.getReferenceLabel(tied, unlimited=True)"
selected=":inRequest and (uid in requestValue) or \
(uid in uids)" value=":uid"
title=":title">:ztool.truncateValue(title, field.width)</option>
</select>
<x if="field.link == 'popup'">:field.pxEditPopup</x></x>''')
pxSearch = Px(''' pxSearch = Px('''
<!-- The "and" / "or" radio buttons --> <!-- The "and" / "or" radio buttons -->
@ -1243,6 +1258,42 @@ class Ref(Field):
if (layoutType == 'view') and (self.render == 'menus'): return 'list' if (layoutType == 'view') and (self.render == 'menus'): return 'list'
return self.render 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): def onUiRequest(self, obj, rq):
'''This method is called when an action tied to this Ref field is '''This method is called when an action tied to this Ref field is
triggered from the user interface (link, unlink, link_many, triggered from the user interface (link, unlink, link_many,
@ -1261,8 +1312,7 @@ class Ref(Field):
# operation. # operation.
obj.mayEdit(self.writePermission, raiseError=True) obj.mayEdit(self.writePermission, raiseError=True)
# Get the (un-)checked objects from the request. # Get the (un-)checked objects from the request.
uids = rq['targetUid'].strip(',') or () uids = rq['targetUid'].split(',')
if uids: uids = uids.split(',')
unchecked = rq['semantics'] == 'unchecked' unchecked = rq['semantics'] == 'unchecked'
if action == 'link_many': if action == 'link_many':
# Get possible values (objects) # Get possible values (objects)

View file

@ -356,7 +356,7 @@ class ToolMixin(BaseMixin):
brains = self.getPath("/catalog")(**params) brains = self.getPath("/catalog")(**params)
if brainsOnly: if brainsOnly:
# Return brains only. # Return brains only.
if not maxResults: return brains if not maxResults or (maxResults == 'NO_LIMIT'): return brains
else: return brains[:maxResults] else: return brains[:maxResults]
if not maxResults: if not maxResults:
if refField: maxResults = refField.maxPerPage if refField: maxResults = refField.maxPerPage
@ -731,8 +731,8 @@ class ToolMixin(BaseMixin):
res = Search('customSearch', **fields) res = Search('customSearch', **fields)
elif ':' in name: elif ':' in name:
# The search is defined in a Ref field with link=popup # The search is defined in a Ref field with link=popup
refClass, ref = name.split(':') refObject, ref = name.split(':')
res = getattr(self.getAppyClass(refClass), ref).select res = getattr(self.getObject(refObject,appy=True).klass,ref).select
elif name: elif name:
appyClass = self.getAppyClass(className) appyClass = self.getAppyClass(className)
# Search among static searches # Search among static searches

View file

@ -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) { function getAjaxChunk(pos) {
// This function is the callback called by the AJAX machinery (see function // This function is the callback called by the AJAX machinery (see function
// askAjaxChunk below) when an Ajax response is available. // askAjaxChunk below) when an Ajax response is available.
@ -114,13 +128,13 @@ function getAjaxChunk(pos) {
var hook = xhrObjects[pos].hook; var hook = xhrObjects[pos].hook;
if (xhrObjects[pos].xhr.readyState == 1) { if (xhrObjects[pos].xhr.readyState == 1) {
// The request has been initialized: display the waiting radar // The request has been initialized: display the waiting radar
var hookElem = document.getElementById(hook); var hookElem = getAjaxHook(hook);
if (hookElem) if (hookElem)
injectChunk(hookElem, "<div align=\"center\"><img src=\"ui/waiting.gif\"/><\/div>"); injectChunk(hookElem, "<div align=\"center\"><img src=\"ui/waiting.gif\"/><\/div>");
} }
if (xhrObjects[pos].xhr.readyState == 4) { if (xhrObjects[pos].xhr.readyState == 4) {
// We have received the HTML chunk // We have received the HTML chunk
var hookElem = document.getElementById(hook); var hookElem = getAjaxHook(hook);
if (hookElem && (xhrObjects[pos].xhr.status == 200)) { if (hookElem && (xhrObjects[pos].xhr.status == 200)) {
injectChunk(hookElem, xhrObjects[pos].xhr.responseText); injectChunk(hookElem, xhrObjects[pos].xhr.responseText);
// Call a custom Javascript function if required // 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. directly on the object at p_url.
p_hook is the ID of the XHTML element that will be filled with the XHTML 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. 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. 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, 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. // Sends an Ajax request for getting the content of any field.
var fieldName = hookId.split('_')[1]; var fieldName = hookId.split('_')[1];
var params = {'layoutType': layoutType, 'showChanges': showChanges}; var params = {'layoutType': layoutType, 'showChanges': showChanges};
if (customParams){for (var key in customParams) params[key]=customParams[key]}
if (masterValues) params['masterValues'] = masterValues.join('*'); if (masterValues) params['masterValues'] = masterValues.join('*');
if (requestValue) params[fieldName] = requestValue; if (requestValue) params[fieldName] = requestValue;
if (error) params[fieldName + '_error'] = error; if (error) params[fieldName + '_error'] = error;
@ -575,17 +591,23 @@ function onLink(action, sourceUid, fieldName, targetUid) {
f.submit(); 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) { function onLinkMany(action, id) {
var elems = _rsplit(id, '_', 3); var elems = _rsplit(id, '_', 3);
// Get the DOM node corresponding to the Ref field. // Get the DOM node corresponding to the Ref field.
var node = document.getElementById(elems[0] + '_' + elems[1]); var node = document.getElementById(elems[0] + '_' + elems[1]);
// Get the uids of (un-)checked objects. // Get the uids of (un-)checked objects.
var statuses = node['_appy_' + elems[2] + '_cbs']; var statuses = node['_appy_' + elems[2] + '_cbs'];
var uids = ''; var uids = stringFromDictKeys(statuses);
for (var uid in statuses) uids += uid + ',';
// Get the array semantics // Get the array semantics
var semantics = node['_appy_' + elems[2] + '_sem']; 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)) { if ((semantics == 'checked') && (len(statuses) == 0)) {
openPopup('alertPopup', no_elem_selected); openPopup('alertPopup', no_elem_selected);
return; return;
@ -674,9 +696,7 @@ function generatePod(uid, fieldName, template, podFormat, queryData,
// We must collect selected objects from a Ref field. // We must collect selected objects from a Ref field.
var node = document.getElementById(uid + '_' + getChecked); var node = document.getElementById(uid + '_' + getChecked);
if (node && node.hasOwnProperty('_appy_objs_cbs')) { if (node && node.hasOwnProperty('_appy_objs_cbs')) {
var uids = []; f.checkedUids.value = stringFromDictKeys(node['_appy_objs_cbs']);
for (var uid in node['_appy_objs_cbs']) uids.push(uid);
f.checkedUids.value = uids.join();
f.checkedSem.value = node['_appy_objs_sem']; f.checkedSem.value = node['_appy_objs_sem'];
} }
} }
@ -749,9 +769,12 @@ function openPopup(popupId, msg, width, height) {
} }
function closePopup(popupId) { function closePopup(popupId) {
// Close the popup // Get the popup
var container = window.parent.document; var container = null;
if (popupId == 'iframePopup') container = window.parent.document;
else container = window.document;
var popup = container.getElementById(popupId); var popup = container.getElementById(popupId);
// Close the popup
popup.style.display = 'none'; popup.style.display = 'none';
popup.style.width = null; popup.style.width = null;
if (popupId == 'iframePopup') { if (popupId == 'iframePopup') {
@ -1004,3 +1027,33 @@ function onSelectDate(cal) {
cal.callCloseHandler(); 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();
}

View file

@ -31,12 +31,17 @@ class ToolWrapper(AbstractWrapper):
(q(field.name), q('desc'), q(filterKey)))" (q(field.name), q('desc'), q(filterKey)))"
src=":url('sortUp.gif')" class="clickable"/> src=":url('sortUp.gif')" class="clickable"/>
</x> </x>
<x if="filterable"> <x if="filterable"
<input type="text" size="7" id=":'%s_%s' % (ajaxHookId, field.name)" var2="filterId='%s_%s' % (ajaxHookId, field.name);
value=":filterKey == field.name and filterValue or ''"/> filterIdIcon='%s_icon' % filterId">
<img onclick=":navBaseCall.replace('**v**', '0, %s,%s,%s' % \ <!-- Pressing the "enter" key in the field clicks the icon (onkeydown)-->
(q(sortKey), q(sortOrder), q(field.name)))" <input type="text" size="7" id=":filterId"
src=":url('funnel')" class="clickable"/> value=":filterKey == field.name and filterValue or ''"
onkeydown=":'if (event.keyCode==13) document.getElementById ' \
'(%s).click()' % q(filterIdIcon)"/>
<img id=":filterIdIcon" class="clickable" src=":url('funnel')"
onclick=":navBaseCall.replace('**v**', '0, %s,%s,%s' % \
(q(sortKey), q(sortOrder), q(field.name)))"/>
</x>''') </x>''')
# Buttons for navigating among a list of objects (from a Ref field or a # 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()</span> name="subTitle">::zobj.getSubTitle()</span>
<!-- Actions --> <!-- Actions -->
<table class="noStyle" if="zobj.mayAct()"> <table class="noStyle" if="not inPopup and zobj.mayAct()">
<tr> <tr>
<!-- Edit --> <!-- Edit -->
<td if="zobj.mayEdit()"> <td if="zobj.mayEdit()">
@ -383,6 +388,9 @@ class ToolWrapper(AbstractWrapper):
</tr> </tr>
<!-- Results --> <!-- Results -->
<tr if="not zobjects">
<td colspan=":len(columns)+1">:_('query_no_result')</td>
</tr>
<tr for="zobj in zobjects" id="query_row" valign="top" <tr for="zobj in zobjects" id="query_row" valign="top"
var2="@currentNumber=currentNumber + 1; var2="@currentNumber=currentNumber + 1;
obj=zobj.appy(); mayView=zobj.mayView()" obj=zobj.appy(); mayView=zobj.mayView()"
@ -398,10 +406,22 @@ class ToolWrapper(AbstractWrapper):
</td> </td>
</tr> </tr>
</table> </table>
<!-- The button for selecting objects and closing the popup. -->
<div if="inPopup" align=":dright">
<input type="button" class="button"
var="label=_('object_link_many');
initiator=ztool.getObject(searchName.split(':')[0], \
appy=True)"
value=":label"
onclick=":'onSelectObjects(%s,%s,%s,%s,%s,%s)' % (q(rootHookId), \
q(initiator.url), q(sortKey), q(sortOrder), \
q(filterKey), q(filterValue))"
style=":'%s; %s' % (url('linkMany', bg=True), \
ztool.getButtonWidth(label))"/>
</div>
<!-- Init checkboxes if present. --> <!-- Init checkboxes if present. -->
<script if="checkboxes" <script if="checkboxes">:'initCbs(%s)' % q(checkboxesId)</script>
type="text/javascript">:'initCbs(%s)' % q(checkboxesId) <script>:'initFocus(%s)' % q(ajaxHookId)</script></x>''')
</script></x>''')
# Show query results as a grid. # Show query results as a grid.
pxQueryResultGrid = Px(''' pxQueryResultGrid = Px('''
@ -426,7 +446,7 @@ class ToolWrapper(AbstractWrapper):
_=ztool.translate; _=ztool.translate;
className=req['className']; className=req['className'];
searchName=req.get('search', ''); searchName=req.get('search', '');
rootHookId=rootHookId|'%s_search' % className; rootHookId=rootHookId|searchName.replace(':', '_');
uiSearch=uiSearch|ztool.getSearch(className,searchName,ui=True); uiSearch=uiSearch|ztool.getSearch(className,searchName,ui=True);
refInfo=ztool.getRefInfo(); refInfo=ztool.getRefInfo();
refObject=refInfo[0]; refObject=refInfo[0];
@ -452,7 +472,7 @@ class ToolWrapper(AbstractWrapper):
(q(ajaxHookId), q(ztool.absolute_url()), q(className), \ (q(ajaxHookId), q(ztool.absolute_url()), q(className), \
q(searchName),int(inPopup)); q(searchName),int(inPopup));
showNewSearch=showNewSearch|True; showNewSearch=showNewSearch|True;
enableLinks=enableLinks|True; enableLinks=enableLinks|not inPopup;
newSearchUrl='%s/search?className=%s%s' % \ newSearchUrl='%s/search?className=%s%s' % \
(ztool.absolute_url(), className, refUrlPart); (ztool.absolute_url(), className, refUrlPart);
showSubTitles=req.get('showSubTitles', 'true') == 'true'; showSubTitles=req.get('showSubTitles', 'true') == 'true';
@ -460,7 +480,7 @@ class ToolWrapper(AbstractWrapper):
target=ztool.getLinksTargetInfo(ztool.getAppyClass(className))" target=ztool.getLinksTargetInfo(ztool.getAppyClass(className))"
id=":ajaxHookId"> id=":ajaxHookId">
<x if="zobjects"> <x if="zobjects or filterValue">
<!-- Display here POD templates if required. --> <!-- Display here POD templates if required. -->
<table var="fields=ztool.getResultPodFields(className); <table var="fields=ztool.getResultPodFields(className);
layoutType='view'" layoutType='view'"
@ -502,7 +522,7 @@ class ToolWrapper(AbstractWrapper):
<x>:tool.pxNavigate</x> <x>:tool.pxNavigate</x>
</x> </x>
<x if="not zobjects"> <x if="not zobjects and not filterValue">
<x>:_('query_no_result')</x> <x>:_('query_no_result')</x>
<x if="showNewSearch and (searchName == 'customSearch')"><br/> <x if="showNewSearch and (searchName == 'customSearch')"><br/>
<i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x> <i class="discreet"><a href=":newSearchUrl">:_('search_new')</a></i></x>
@ -513,12 +533,12 @@ class ToolWrapper(AbstractWrapper):
<div var="className=req['className']; <div var="className=req['className'];
searchName=req.get('search', ''); searchName=req.get('search', '');
uiSearch=ztool.getSearch(className, searchName, ui=True); uiSearch=ztool.getSearch(className, searchName, ui=True);
rootHookId='%s_search' % className; rootHookId=searchName.replace(':', '_');
cssJs=None" cssJs=None"
id=":rootHookId"> id=":rootHookId">
<script type="text/javascript">:uiSearch.search.getCbJsInit(rootHookId) <script type="text/javascript">:uiSearch.search.getCbJsInit(rootHookId)
</script> </script>
<x if="not inPopup">:tool.pxPagePrologue</x><x>:tool.pxQueryResult</x> <x>:tool.pxPagePrologue</x><x>:tool.pxQueryResult</x>
</div>''', template=AbstractWrapper.pxTemplate, hook='content') </div>''', template=AbstractWrapper.pxTemplate, hook='content')
pxSearch = Px(''' pxSearch = Px('''