Reworked AJAX framework with a lot of new sorting and filtering possibilities.

This commit is contained in:
Gaetan Delannay 2010-04-30 12:05:29 +02:00
parent 46cda3f755
commit fd775e17a2
11 changed files with 851 additions and 890 deletions

View file

@ -1,7 +1,7 @@
# ------------------------------------------------------------------------------
import re, time
from appy.shared.utils import Traceback
from appy.gen.utils import sequenceTypes, PageDescr
from appy.gen.utils import sequenceTypes, PageDescr, Keywords
from appy.shared.data import countries
# Default Appy permissions -----------------------------------------------------
@ -54,6 +54,56 @@ class Search:
self.limit = limit
self.fields = fields # This is a dict whose keys are indexed field
# names and whose values are search values.
@staticmethod
def getIndexName(fieldName, usage='search'):
'''Gets the name of the technical index that corresponds to field named
p_fieldName. Indexes can be used for searching (p_usage="search") or
for sorting (usage="sort"). The method returns None if the field
named p_fieldName can't be used for p_usage.'''
if fieldName == 'title':
if usage == 'search': return 'Title'
else: return 'sortable_title'
# Indeed, for field 'title', Plone has created a specific index
# 'sortable_title', because index 'Title' is a ZCTextIndex
# (for searchability) and can't be used for sorting.
elif fieldName == 'description':
if usage == 'search': return 'Description'
else: return None
elif fieldName == 'state': return 'review_state'
else:
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
@staticmethod
def getSearchValue(fieldName, fieldValue):
'''Returns a transformed p_fieldValue for producing a valid search
value as required for searching in the index corresponding to
p_fieldName.'''
if fieldName == 'title':
# Title is a ZCTextIndex. We must split p_fieldValue into keywords.
res = Keywords(fieldValue.decode('utf-8')).get()
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
v = fieldValue[:-1]
# Warning: 'z' is higher than 'Z'!
res = {'query':(v,v+'z'), 'range':'min:max'}
elif type(fieldValue) in sequenceTypes:
if fieldValue and isinstance(fieldValue[0], basestring):
# We have a list of string values (ie: we need to
# search v1 or v2 or...)
res = fieldValue
else:
# We have a range of (int, float, DateTime...) values
minv, maxv = fieldValue
rangev = 'minmax'
queryv = fieldValue
if minv == None:
rangev = 'max'
queryv = maxv
elif maxv == None:
rangev = 'min'
queryv = minv
res = {'query':queryv, 'range':rangev}
else:
res = fieldValue
return res
# ------------------------------------------------------------------------------
class Type:

View file

@ -105,10 +105,11 @@ class ToolMixin(AbstractMixin):
if res: return res[0].getObject()
return None
_sortFields = {'title': 'sortable_title'}
def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0, search=None, remember=False,
brainsOnly=False, maxResults=None, noSecurity=False):
brainsOnly=False, maxResults=None, noSecurity=False,
sortBy=None, sortOrder='asc',
filterKey=None, filterValue=None):
'''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. Portal types are from the
flavour numbered p_flavourNumber. If p_searchName is specified, it
@ -136,7 +137,16 @@ class ToolMixin(AbstractMixin):
p_maxResults equals string "NO_LIMIT".
If p_noSecurity is True, it gets all the objects, even those that the
currently logged user can't see.'''
currently logged user can't see.
The result is sorted according to the potential sort key defined in
the Search instance (Search.sortBy). But if parameter p_sortBy is
given, it defines or overrides the sort. In this case, p_sortOrder
gives the order (*asc*ending or *desc*ending).
If p_filterKey is given, it represents an additional search parameter
to take into account: the corresponding search value is in
p_filterValue.'''
# Is there one or several content types ?
if contentType.find(',') != -1:
# Several content types are specified
@ -149,10 +159,9 @@ class ToolMixin(AbstractMixin):
params = {'portal_type': portalTypes}
if not brainsOnly: params['batch'] = True
# Manage additional criteria from a search when relevant
if searchName or search:
if searchName:
# In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType)
if searchName:
if searchName != '_advanced':
search = ArchetypesClassDescriptor.getSearch(
appyClass, searchName)
@ -164,44 +173,26 @@ class ToolMixin(AbstractMixin):
for fieldName, fieldValue in search.fields.iteritems():
# Make the correspondance between the name of the field and the
# name of the corresponding index.
attrName = fieldName
if attrName == 'title': attrName = 'Title'
elif attrName == 'description': attrName = 'Description'
elif attrName == 'state': attrName = 'review_state'
else: attrName = 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
attrName = Search.getIndexName(fieldName)
# Express the field value in the way needed by the index
if isinstance(fieldValue, basestring) and \
fieldValue.endswith('*'):
v = fieldValue[:-1]
params[attrName] = {'query':(v,v+'z'), 'range':'min:max'}
# Warning: 'z' is higher than 'Z'!
elif type(fieldValue) in sequenceTypes:
if fieldValue and isinstance(fieldValue[0], basestring):
# We have a list of string values (ie: we need to
# search v1 or v2 or...)
params[attrName] = fieldValue
else:
# We have a range of (int, float, DateTime...) values
minv, maxv = fieldValue
rangev = 'minmax'
queryv = fieldValue
if minv == None:
rangev = 'max'
queryv = maxv
elif maxv == None:
rangev = 'min'
queryv = minv
params[attrName] = {'query':queryv, 'range':rangev}
else:
params[attrName] = fieldValue
params[attrName] = Search.getSearchValue(fieldName, fieldValue)
# Add a sort order if specified
sb = search.sortBy
if sb:
# For field 'title', Plone has created a specific index
# 'sortable_title', because index 'Title' is a ZCTextIndex
# (for searchability) and can't be used for sorting.
if self._sortFields.has_key(sb): sb = self._sortFields[sb]
params['sort_on'] = sb
sortKey = search.sortBy
if sortKey:
params['sort_on'] = Search.getIndexName(sortKey, usage='sort')
# Determine or override sort if specified.
if sortBy:
params['sort_on'] = Search.getIndexName(sortBy, usage='sort')
if sortOrder == 'desc': params['sort_order'] = 'reverse'
else: params['sort_order'] = None
# If defined, add the filter among search parameters.
if filterKey:
filterKey = Search.getIndexName(filterKey)
filterValue = Search.getSearchValue(filterKey, filterValue)
params[filterKey] = filterValue
# TODO This value needs to be merged with an existing one if already
# in params, or, in a first step, we should avoid to display the
# corresponding filter widget on the screen.
# Determine what method to call on the portal catalog
if noSecurity: catalogMethod = 'unrestrictedSearchResults'
else: catalogMethod = 'searchResults'
@ -544,12 +535,11 @@ class ToolMixin(AbstractMixin):
if cookieValue: return cookieValue.value
return default
def getQueryUrl(self, contentType, flavourNumber, searchName, ajax=True,
def getQueryUrl(self, contentType, flavourNumber, searchName,
startNumber=None):
'''This method creates the URL that allows to perform an ajax GET
'''This method creates the URL that allows to perform a (non-Ajax)
request for getting queried objects from a search named p_searchName
on p_contentType from flavour numbered p_flavourNumber. If p_ajax
is False, it returns the non-ajax URL.'''
on p_contentType from flavour numbered p_flavourNumber.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams= 'type_name=%s&flavourNumber=%s' %(contentType,flavourNumber)
# Manage start number
@ -559,11 +549,7 @@ class ToolMixin(AbstractMixin):
elif rq.has_key('startNumber'):
baseParams += '&startNumber=%s' % rq['startNumber']
# Manage search name
if searchName or ajax: baseParams += '&search=%s' % searchName
if ajax:
return '%s/ajax?objectUid=%s&page=macros&macro=queryResult&%s' % \
(baseUrl, self.UID(), baseParams)
else:
if searchName: baseParams += '&search=%s' % searchName
return '%s/query?%s' % (baseUrl, baseParams)
def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize):
@ -666,7 +652,7 @@ class ToolMixin(AbstractMixin):
startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber,
searchName, ajax=False, startNumber=startNumber)
searchName, startNumber=startNumber)
# Compute URLs
for urlType in ('previous', 'next', 'first', 'last'):
exec 'needIt = %sNeeded' % urlType

View file

@ -659,6 +659,15 @@ class AbstractMixin:
isDelta = True
self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta)
def onSortReference(self):
'''This method is called when the user wants to sort the content of a
reference field.'''
rq = self.REQUEST
fieldName = rq.get('fieldName')
sortKey = rq.get('sortKey')
reverse = rq.get('reverse') == 'True'
self.appy().sort(fieldName, sortKey=sortKey, reverse=reverse)
def getWorkflow(self, appy=True):
'''Returns the Appy workflow instance that is relevant for this
object. If p_appy is False, it returns the DC workflow.'''
@ -1243,27 +1252,9 @@ class AbstractMixin:
exec 'self.set%s%s([])' % (fieldName[0].upper(),
fieldName[1:])
def getUrl(self, t='view', **kwargs):
'''This method returns various URLs about this object.'''
baseUrl = self.absolute_url()
params = ''
rq = self.REQUEST
for k, v in kwargs.iteritems(): params += '&%s=%s' % (k, v)
if params: params = params[1:]
if t == 'showRef':
chunk = '/skyn/ajax?objectUid=%s&page=ref&' \
'macro=showReferenceContent&' % self.UID()
startKey = '%s%s_startNumber' % (self.UID(), kwargs['fieldName'])
if rq.has_key(startKey) and not kwargs.has_key(startKey):
params += '&%s=%s' % (startKey, rq[startKey])
return baseUrl + chunk + params
elif t == 'showHistory':
chunk = '/skyn/ajax?objectUid=%s&page=macros&macro=history' % \
self.UID()
if params: params = '&' + params
return baseUrl + chunk + params
else: # We consider t=='view'
return baseUrl + '/skyn/view' + params
def getUrl(self):
'''Returns the Appy URL for viewing this object.'''
return self.absolute_url() + '/skyn/view'
def translate(self, label, mapping={}, domain=None, default=None):
'''Translates a given p_label into p_domain with p_mapping.'''

View file

@ -1,15 +1,12 @@
<tal:comment replace="nothing">
This page is called by a XmlHttpRequest object. It requires parameters "page" and "macro":
they are used to call the macro that will render the HTML chunk to be returned to the browser.
It also requires parameters "objectUid", which is the UID of the related object. The object will
be available to the macro as "contextObj".
It can also have a parameter "action", that refers to a method that will be triggered on
contextObj before returning the result of the macro to the browser.
</tal:comment>
<tal:ajax define="page request/page;
macro request/macro;
macroPath python: 'here/%s/macros/%s' % (page, macro);
contextObj python: context.uid_catalog(UID=request['objectUid'])[0].getObject();
contextObj context/getParentNode;
action request/action|nothing;
response request/RESPONSE;
member context/portal_membership/getAuthenticatedMember;
@ -18,8 +15,11 @@
dummy python:response.setHeader('Content-Type','text/html;;charset=utf-8');
dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');
dummy3 python:response.setHeader('CacheControl', 'no-cache')">
<tal:comment replace="nothing">Keys "Expires" and "CacheControl" are used for preventing IE to cache
this page. Indeed, this page is retrieved through an asynchronous XMLHttpRequest by the browser, and
IE caches this by default.</tal:comment>
<tal:executeAction condition="action">
<tal:do define="dummy python: contextObj.getAppyValue('on'+action)()" omit-tag=""/>
</tal:executeAction>
<metal:callMacro use-macro="python: context.get(page).macros.get(macro)"/>
<metal:callMacro use-macro="python: portal.skyn.get(page).macros.get(macro)"/>
</tal:ajax>

View file

@ -295,12 +295,12 @@
batchSize historyInfo/batchSize;
totalNumber historyInfo/totalNumber;
ajaxHookId python:'appyHistory';
baseUrl python: contextObj.getUrl('showHistory', startNumber='**v**');
navBaseCall python: 'askObjectHistory(\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url());
tool contextObj/getTool">
<tal:comment replace="nothing">Table containing the history</tal:comment>
<tal:history condition="objs">
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/>
<table width="100%" class="listing nosort">
<tr i18n:domain="plone">
<th i18n:translate="listingheader_action"/>
@ -370,8 +370,11 @@
this.xhr = false;
if (window.XMLHttpRequest) this.xhr = new XMLHttpRequest();
else this.xhr = new ActiveXObject("Microsoft.XMLHTTP");
this.hook = ''; // The ID of the HTML element in the page that will be
// replaced by result of executing the Ajax request.
this.hook = ''; /* The ID of the HTML element in the page that will be
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. */
}
function getAjaxChunk(pos) {
@ -391,15 +394,35 @@
var hookElem = document.getElementById(hook);
if (hookElem && (xhrObjects[pos].xhr.status == 200)) {
hookElem.innerHTML = xhrObjects[pos].xhr.responseText;
// Call a custom Javascript function if required
if (xhrObjects[pos].onGet) {
xhrObjects[pos].onGet(xhrObjects[pos], hookElem);
}
}
xhrObjects[pos].freed = 1;
}
}
}
function askAjaxChunk(hook, url) {
// This function will ask to get a chunk of HTML on the server by
// triggering a XMLHttpRequest.
function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
/* This function will ask to get a chunk of HTML on the server through a
XMLHttpRequest. p_mode can be 'GET' or 'POST'. p_url is the URL of a
given server object. On this URL we will call the page "ajax.pt" that
will call a specific p_macro in a given p_page with some additional
p_params (must be an associative array) if required.
p_hook is the ID of the HTML element that will be filled with the HTML
result from the server.
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 method can return, in a string, additional parameters to
send, ie: "&param1=blabla&param2=blabla".
p_onGet is a Javascript function to call when we will receive the answer.
This function will get 2 args, too: the XMLHttpRequest object and the
HTML node element into which the result has been inserted.
*/
// First, get a non-busy XMLHttpRequest object.
var pos = -1;
for (var i=0; i < xhrObjects.length; i++) {
@ -410,14 +433,84 @@
xhrObjects[pos] = new XhrObject();
}
xhrObjects[pos].hook = hook;
xhrObjects[pos].onGet = onGet;
if (xhrObjects[pos].xhr) {
xhrObjects[pos].freed = 0;
// Perform the asynchronous HTTP GET
xhrObjects[pos].xhr.open('GET', url, true);
xhrObjects[pos].xhr.onreadystatechange = function() { getAjaxChunk(pos); }
if (window.XMLHttpRequest) { xhrObjects[pos].xhr.send(null); }
else if (window.ActiveXObject) { xhrObjects[pos].xhr.send(); }
var rq = xhrObjects[pos];
rq.freed = 0;
// Construct parameters
var paramsFull = 'page=' + page + '&macro=' + macro;
if (params) {
for (var paramName in params)
paramsFull = paramsFull + '&' + paramName + '=' + params[paramName];
}
// Call beforeSend if required
if (beforeSend) {
var res = beforeSend(rq, params);
if (res) paramsFull = paramsFull + res;
}
// Construct the URL to call
var urlFull = url + '/skyn/ajax';
if (mode == 'GET') {
urlFull = urlFull + '?' + paramsFull;
}
// Perform the asynchronous HTTP GET or POST
rq.xhr.open(mode, urlFull, true);
if (mode == 'POST') {
// Set the correct HTTP headers
rq.xhr.setRequestHeader(
"Content-Type", "application/x-www-form-urlencoded");
rq.xhr.setRequestHeader("Content-length", paramsFull.length);
rq.xhr.setRequestHeader("Connection", "close");
rq.xhr.onreadystatechange = function(){ getAjaxChunk(pos); }
rq.xhr.send(paramsFull);
}
else if (mode == 'GET') {
rq.xhr.onreadystatechange = function() { getAjaxChunk(pos); }
if (window.XMLHttpRequest) { rq.xhr.send(null); }
else if (window.ActiveXObject) { rq.xhr.send(); }
}
}
}
/* The functions below wrap askAjaxChunk for getting specific content through
an Ajax request. */
function askQueryResult(hookId, objectUrl, contentType, flavourNumber,
searchName, startNumber, sortKey, sortOrder, filterKey) {
// Sends an Ajax request for getting the result of a query.
var params = {'type_name': contentType, 'flavourNumber': flavourNumber,
'search': searchName, 'startNumber': startNumber};
if (sortKey) params['sortKey'] = sortKey;
if (sortOrder) params['sortOrder'] = sortOrder;
if (filterKey) {
var filterWidget = document.getElementById(hookId + '_' + filterKey);
if (filterWidget && filterWidget.value) {
params['filterKey'] = filterKey;
params['filterValue'] = filterWidget.value;
}
}
askAjaxChunk(hookId,'GET',objectUrl,'macros','queryResult',params);
}
function askObjectHistory(hookId, objectUrl, startNumber) {
// Sends an Ajax request for getting the history of an object
var params = {'startNumber': startNumber};
askAjaxChunk(hookId, 'GET', objectUrl, 'macros', 'history', params);
}
function askRefField(hookId, objectUrl, fieldName, isBack, innerRef, labelId,
descrId, startNumber, action, actionParams){
// Sends an Ajax request for getting the content of a reference field.
var startKey = hookId + '_startNumber';
var params = {'fieldName': fieldName, 'isBack': isBack,
'innerRef': innerRef, 'labelId': labelId,
'descrId': descrId };
params[startKey] = startNumber;
if (action) params['action'] = action;
if (actionParams) {
for (key in actionParams) { params[key] = actionParams[key]; };
}
askAjaxChunk(hookId, 'GET', objectUrl, 'ref', 'showReferenceContent',
params);
}
// Function used by checkbox widgets for having radio-button-like behaviour
@ -633,10 +726,9 @@
<td colspan="2">
<span id="appyHistory"
tal:attributes="style python:test(historyExpanded, 'display:block', 'display:none')">
<div tal:define="ajaxHookId python: contextObj.UID() + '_history';
ajaxUrl python: contextObj.getUrl('showHistory')"
<div tal:define="ajaxHookId python: contextObj.UID() + '_history';"
tal:attributes="id ajaxHookId">
<script language="javascript" tal:content="python: 'askAjaxChunk(\'%s\',\'%s\')' % (ajaxHookId, ajaxUrl)">
<script language="javascript" tal:content="python: 'askObjectHistory(\'%s\',\'%s\',0)' % (ajaxHookId, contextObj.absolute_url())">
</script>
</div>
</span>
@ -655,7 +747,7 @@
</td>
</tr>
</table>
<metal:nav use-macro="here/skyn/macros/macros/objectNavigate"/>
<metal:nav use-macro="here/skyn/navigate/macros/objectNavigate"/>
<tal:comment replace="nothing">Tabs</tal:comment>
<ul class="contentViews appyTabs" tal:condition="python: len(appyPages)&gt;1">
@ -726,12 +818,16 @@
searchLabel python: test(searchName=='_advanced', 'search_results', '%s_search_%s' % (contentType, searchName));
searchDescr python: '%s_descr' % searchLabel;
severalTypes python: contentType and (contentType.find(',') != -1);
queryResult python: tool.executeQuery(contentType, flavourNumber, searchName, startNumber, remember=True);
sortKey request/sortKey| python:'';
sortOrder request/sortOrder| python:'asc';
filterKey request/filterKey| python:'';
filterValue request/filterValue | python:'';
queryResult python: tool.executeQuery(contentType, flavourNumber, searchName, startNumber, remember=True, sortBy=sortKey, sortOrder=sortOrder, filterKey=filterKey, filterValue=filterValue);
objs queryResult/objects;
totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize;
ajaxHookId python:'queryResult';
baseUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName, startNumber='**v**');
navBaseCall python: 'askQueryResult(\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, tool.absolute_url(), contentType, flavourNumber, searchName);
newSearchUrl python: '%s/skyn/search?type_name=%s&flavourNumber=%d' % (tool.getAppFolder().absolute_url(), contentType, flavourNumber);">
<tal:result condition="objs">
@ -753,7 +849,7 @@
</td>
<td align="right" width="25%">
<tal:comment replace="nothing">Appy (top) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/>
</td>
</tr></table>
@ -763,26 +859,20 @@
excepted for workflow state (which is not a field): in this case it is simply the
string "workflowState".</tal:comment>
<tal:comment replace="nothing">Headers, with (disabled) filters and sort arrows</tal:comment>
<tal:comment replace="nothing">Headers, with filters and sort arrows</tal:comment>
<tr>
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment>
<th><!--img tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'title\')';"
id="arrow_title" style="cursor:pointer"/-->
<th tal:define="fieldName python:'title'; sortable python:True; filterable python:True">
<span tal:content="python: tool.translate('ref_name')"/>
<!--input id="filter_title" type="text" size="5" onkeyup="javascript:onTextEntered('title')"/-->
<tal:comment replace="nothing">Input fields like this have been commented out because they will
be replaced by Ajax server- searches that will be more relevant (the current Javascript search
is limited to the batch, which has little interest).</tal:comment>
<metal:sortAndFilter use-macro="here/skyn/navigate/macros/sortAndFilter"/>
</th>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
<tal:columnHeader repeat="fieldDescr fieldDescrs">
<th tal:define="fieldName fieldDescr/atField/getName|string:workflow_state">
<!--img tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'%s\')' % fieldName;
id python: 'arrow_%s' % fieldName"
style="cursor:pointer"/-->
<th tal:define="fieldName fieldDescr/atField/getName|string:workflow_state;
sortable fieldDescr/sortable|nothing;
filterable fieldDescr/filterable|nothing;">
<tal:comment replace="nothing">Display header for a "standard" field</tal:comment>
<tal:standardField condition="python: fieldName != 'workflow_state'">
<span tal:replace="python: tool.translate(fieldDescr['atField'].widget.label_msgid)"/>
@ -791,20 +881,13 @@
<tal:workflowState condition="python: fieldName == 'workflow_state'">
<span tal:replace="python: tool.translate('workflow_state')"/>
</tal:workflowState>
<!--input type="text" size="5"
tal:attributes="id python: 'filter_%s' % fieldName;
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/-->
<metal:sortAndFilter use-macro="here/skyn/navigate/macros/sortAndFilter"/>
</th>
</tal:columnHeader>
<tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<th tal:condition="severalTypes"><!--img
tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'root_type\')';"
id = "arrow_root_type" style="cursor:pointer"/-->
<th tal:condition="severalTypes">
<span tal:replace="python: tool.translate('root_type')"/>
<!--input type="text" size="5" id="filter_root_type"
tal:attributes="onkeyup python:'javascript:onTextEntered(\'root_type\')'"/-->
</th>
<tal:comment replace="nothing">Column "Actions"</tal:comment>
@ -872,7 +955,7 @@
</table>
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/>
</fieldset>
</tal:result>
@ -952,91 +1035,3 @@
</table>
</form>
</metal:transitions>
<div metal:define-macro="appyNavigate" tal:condition="python: totalNumber &gt; batchSize" align="right">
<tal:comment replace="nothing">
Buttons for navigating among a list of elements (next, back, first, last, etc).
</tal:comment>
<table cellpadding="0" cellspacing="0" class="appyNav">
<tr>
<tal:comment replace="nothing">Go to the first page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != 0) and (startNumber != batchSize)"
tal:attributes="src string: $portal_url/skyn/arrowLeftDouble.png;
title python: tool.translate('goto_first');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', '0'))"/></td>
<tal:comment replace="nothing">Go to the previous page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: startNumber != 0"
tal:define="sNumber python: startNumber - batchSize"
tal:attributes="src string: $portal_url/skyn/arrowLeftSimple.png;
title python: tool.translate('goto_previous');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', str(sNumber)))"/></td>
<tal:comment replace="nothing">Explain which elements are currently shown</tal:comment>
<td class="discreet" valign="middle">&nbsp;
<span tal:replace="python: startNumber+1"/>
<img tal:attributes="src string: $portal_url/skyn/to.png"/>
<span tal:replace="python: startNumber+len(objs)"/>&nbsp;<b>//</b>
<span tal:replace="python: totalNumber"/>&nbsp;&nbsp;
</td>
<tal:comment replace="nothing">Go to the next page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: sNumber &lt; totalNumber"
tal:define="sNumber python: startNumber + batchSize"
tal:attributes="src string: $portal_url/skyn/arrowRightSimple.png;
title python: tool.translate('goto_next');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', str(sNumber)))"/></td>
<tal:comment replace="nothing">Go to the last page</tal:comment>
<td><img style="cursor:pointer" tal:condition="python: (startNumber != sNumber) and (startNumber != sNumber-batchSize)"
tal:define="lastPageIsIncomplete python: totalNumber % batchSize;
nbOfCompletePages python: totalNumber/batchSize;
nbOfCountedPages python: test(lastPageIsIncomplete, nbOfCompletePages, nbOfCompletePages-1);
sNumber python: (nbOfCountedPages*batchSize)"
tal:attributes="src string: $portal_url/skyn/arrowRightDouble.png;
title python: tool.translate('goto_last');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, baseUrl.replace('**v**', str(sNumber)))"/></td>
</tr>
</table>
</div>
<div metal:define-macro="objectNavigate" tal:condition="request/nav|nothing" align="right">
<tal:comment replace="nothing">
Buttons for going to next/previous elements if this one is among bunch of referenced or searched objects.
currentNumber starts with 1.
</tal:comment>
<table cellpadding="0" cellspacing="0"
tal:define="navInfo tool/getNavigationInfo;
currentNumber navInfo/currentNumber;
totalNumber navInfo/totalNumber;
firstUrl navInfo/firstUrl;
previousUrl navInfo/previousUrl;
nextUrl navInfo/nextUrl;
lastUrl navInfo/lastUrl;
sourceUrl navInfo/sourceUrl;
backText navInfo/backText">
<tr>
<tal:comment replace="nothing">Go to the source URL (search or referred object)</tal:comment>
<td><a tal:condition="sourceUrl" tal:attributes="href sourceUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/gotoSource.png;
title python: backText + ' : ' + tool.translate('goto_source')"/></a></td>
<tal:comment replace="nothing">Go to the first page</tal:comment>
<td><a tal:condition="firstUrl" tal:attributes="href firstUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowLeftDouble.png;
title python: tool.translate('goto_first')"/></a></td>
<tal:comment replace="nothing">Go to the previous page</tal:comment>
<td><a tal:condition="previousUrl" tal:attributes="href previousUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowLeftSimple.png;
title python: tool.translate('goto_previous')"/></a></td>
<tal:comment replace="nothing">Explain which element is currently shown</tal:comment>
<td class="discreet" valign="middle">&nbsp;
<span tal:replace="python: currentNumber"/>&nbsp;<b>//</b>
<span tal:replace="python: totalNumber"/>&nbsp;&nbsp;
</td>
<tal:comment replace="nothing">Go to the next page</tal:comment>
<td><a tal:condition="python: nextUrl" tal:attributes="href nextUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowRightSimple.png;
title python: tool.translate('goto_next')"/></a></td>
<tal:comment replace="nothing">Go to the last page</tal:comment>
<td><a tal:condition="lastUrl" tal:attributes="href lastUrl"><img style="cursor:pointer"
tal:attributes="src string: $portal_url/skyn/arrowRightDouble.png;
title python: tool.translate('goto_last')"/></a></td>
</tr>
</table>
</div>

View file

@ -21,132 +21,12 @@
searchName python:context.REQUEST.get('search', '')">
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/>
<script language="javascript">
<!--
function getSortValue(row, fieldName) {
// Find, from p_fieldName, the cell that is used for sorting.
var cellId = "field_" + fieldName;
var cells = row.cells;
for (var i=0; i < cells.length; i++) {
if (cells[i].id == cellId) {
// Ok we have the cell on which we must sort.
// Now get the cell content.
// If the cell contains links, content is the 1st link content
var innerLinks = cells[i].getElementsByTagName("a");
if (innerLinks.length > 0) {
return innerLinks[0].innerHTML;
} else {
return cells[i].innerHTML;
}
}
}
}
function sortRows(fieldName, ascending) {
var queryRows = cssQuery('#query_row');
// Create a wrapper for sorting
var RowWrapper = function(row, fieldName) {
this.value = getSortValue(row, fieldName);
this.cloned_node = row.cloneNode(true);
this.toString = function() {
if (this.value.toString) {
return this.value.toString();
} else {
return this.value;
}
}
}
// Wrap nodes
var items = new Array();
for (var i=0; i<queryRows.length; i++) {
items.push(new RowWrapper(queryRows[i], fieldName));
}
// Sort nodes
items.sort();
if (!ascending) {
items.reverse();
}
// Reorder nodes
for (var i=0; i<items.length; i++) {
var dest = queryRows[i];
dest.parentNode.replaceChild(items[i].cloned_node, dest);
}
};
function onSort(fieldName){
// First, switch the sort arrow (up->down or down->up)
var arrow = document.getElementById("arrow_" + fieldName);
var sortAscending = (arrow.src.indexOf('arrowDown.gif') != -1);
if (sortAscending){
// Display "up" image
arrow.src = arrow.src.replace('arrowDown.gif', 'arrowUp.gif')
}
else { // Display "down" image
arrow.src = arrow.src.replace('arrowUp.gif', 'arrowDown.gif')
}
// Then, sort the rows on column "fieldName".
sortRows(fieldName, sortAscending);
}
function cellMatches(cell, searchValue) {
// This function returns true if the HTML p_cell contains p_searchValue
var innerLinks = cell.getElementsByTagName("a");
// If the cell contains links, we search within the link contents
for (var i=0; i < innerLinks.length; i++){
var linkContent = innerLinks[i].innerHTML.toLowerCase();
if (linkContent.indexOf(searchValue) != -1) {
return true;
}
}
// If we are here, we still have no match. Let's search directly within
// the cell.
var cellContent = cell.innerHTML.toLowerCase();
if (cellContent.indexOf(searchValue) != -1) {
return true;
}
return false;
}
function onTextEntered(fieldName) {
// Is called whenever text is entered into field named p_fieldName.
var cellId = "field_" + fieldName
var field = document.getElementById("filter_" + fieldName);
var fieldValue = field.value.toLowerCase();
if (fieldValue.length >= 3) {
// Browse all rows and check if it should be visible or not.
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
// Find the value of the cell.
var queryCells = queryRows[i].cells;
for (var j=0; j < queryCells.length; j++) {
if (queryCells[j].id == cellId) {
if (cellMatches(queryCells[j], fieldValue)) {
queryRows[i].style.display = "";
}
else {
queryRows[i].style.display = "none";
}
}
}
}
}
else {
// Show all rows
var queryRows = cssQuery('#query_row');
for (var i=0; i < queryRows.length; i++) {
queryRows[i].style.display = "";
}
}
}
-->
</script>
<tal:comment replace="nothing">Query result</tal:comment>
<div id="queryResult"></div>
<script language="javascript"
tal:define="ajaxUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName)"
tal:content="python: 'askAjaxChunk(\'queryResult\',\'%s\')' % ajaxUrl">
tal:content="python: 'askQueryResult(\'queryResult\', \'%s\',\'%s\',\'%s\',\'%s\',0)' % (tool.absolute_url(), contentType, flavourNumber, searchName)">
</script>
</div>
</body>

View file

@ -21,18 +21,18 @@
<tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment>
<td class="noPadding" tal:condition="python: (len(objs)&gt;1) and member.has_permission('Modify portal content', contextObj)">
<tal:moveRef define="objectIndex python: contextObj.getAppyRefIndex(fieldName, obj);
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId, '%s_startNumber' % ajaxHookId: startNumber, 'action':'ChangeRefOrder', 'refObjectUid': obj.UID()})">
ajaxBaseCall python: navBaseCall.replace('**v**', '\'%s\',\'ChangeRefOrder\', {\'refObjectUid\':\'%s\', \'move\':\'**v**\'}' % (startNumber, obj.UID()))">
<tal:comment replace="nothing">Move up</tal:comment>
<img tal:define="ajaxUrl python: baseUrl + '&move=up'" tal:condition="python: objectIndex &gt; 0"
<img tal:condition="python: objectIndex &gt; 0"
tal:attributes="src string: $portal_url/skyn/arrowUp.png;
title python: tool.translate('move_up');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, ajaxUrl)"
onClick python: ajaxBaseCall.replace('**v**', 'up')"
style="cursor:pointer"/>
<tal:comment replace="nothing">Move down</tal:comment>
<img tal:define="ajaxUrl python: baseUrl + '&move=down'" tal:condition="python: objectIndex &lt; (totalNumber-1)"
<img tal:condition="python: objectIndex &lt; (totalNumber-1)"
tal:attributes="src string: $portal_url/skyn/arrowDown.png;
title python: tool.translate('move_down');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, ajaxUrl)"
onClick python: ajaxBaseCall.replace('**v**', 'down')"
style="cursor:pointer"/>
</tal:moveRef>
</td>
@ -63,6 +63,22 @@
onClick python: 'href: window.location=\'%s/skyn/do?action=Create&initiator=%s&field=%s&type_name=%s\'' % (folder.absolute_url(), contextObj.UID(), fieldName, linkedPortalType)"/>
</metal:plusIcon>
<tal:comment replace="nothing">
This macro displays, in a cell header from a ref table, icons for sorting the
ref field according to the field that corresponds to this column.
</tal:comment>
<metal:sortIcons define-macro="sortIcons"
tal:define="ajaxBaseCall python: navBaseCall.replace('**v**', '\'%s\',\'SortReference\', {\'sortKey\':\'%s\', \'reverse\':\'**v**\'}' % (startNumber, shownField))">
<img style="cursor:pointer"
tal:attributes="src string:$portal_url/skyn/sortAsc.png;
title python: tool.translate('sort_asc');
onClick python: ajaxBaseCall.replace('**v**', 'False')"/>
<img style="cursor:pointer"
tal:attributes="src string:$portal_url/skyn/sortDesc.png;
title python: tool.translate('sort_desc');
onClick python: ajaxBaseCall.replace('**v**', 'True')"/>
</metal:sortIcons>
<tal:comment replace="nothing">
This macro shows a reference field. More precisely, it shows nothing, but calls
a Javascript function that will asynchonously call (via a XmlHttpRequest object) the
@ -77,11 +93,10 @@
- descrId (string) the i18n id of the reference field description
</tal:comment>
<div metal:define-macro="showReference"
tal:define="ajaxHookId python: contextObj.UID() + fieldName;
ajaxUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId})"
tal:define="ajaxHookId python: contextObj.UID() + fieldName"
tal:attributes="id ajaxHookId">
<script language="javascript"
tal:content="python: 'askAjaxChunk(\'%s\',\'%s\')' % (ajaxHookId, ajaxUrl)">
tal:content="python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',0)' % (ajaxHookId, contextObj.absolute_url(), fieldName, isBack, innerRef, labelId, descrId)">
</script>
</div>
@ -113,7 +128,7 @@
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(labelId);
description python: tool.translate(descrId);
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId, '%s_startNumber' % ajaxHookId: '**v**'})">
navBaseCall python: 'askRefField(\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url(), fieldName, isBack, innerRef, labelId, descrId)">
<tal:comment replace="nothing">This macro displays the Reference widget on a "consult" page.
@ -159,7 +174,7 @@
tal:content="description" class="discreet" ></p>
<tal:comment replace="nothing">Appy (top) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/>
<tal:comment replace="nothing">No object is present</tal:comment>
<p tal:condition="not:objs" tal:content="python: tool.translate('no_ref')"></p>
@ -183,14 +198,21 @@
align="right" tal:condition="python: not isBack and objs" cellpadding="0" cellspacing="0">
<tr tal:condition="appyType/showHeaders">
<th tal:condition="python: 'title' not in appyType['shownInfo']"
tal:content="python: tool.translate('ref_name')"></th>
tal:define="shownField python:'title'">
<span tal:content="python: tool.translate('ref_name')"></span>
<metal:sortIcons use-macro="here/skyn/ref/macros/sortIcons" />
</th>
<th tal:repeat="shownField appyType/shownInfo">
<tal:showHeader condition="python: objs[0].getField(shownField)">
<tal:titleHeader condition="python: shownField == 'title'"
content="python: tool.translate('ref_name')"/>
<tal:titleHeader condition="python: shownField == 'title'">
<span tal:content="python: tool.translate('ref_name')"></span>
<metal:sortIcons use-macro="here/skyn/ref/macros/sortIcons" />
</tal:titleHeader>
<tal:otherHeader condition="python: shownField != 'title'"
define="labelId python: objs[0].getField(shownField).widget.label_msgid"
content="python: tool.translate(labelId)"/>
define="labelId python: objs[0].getField(shownField).widget.label_msgid">
<span tal:content="python: tool.translate(labelId)"></span>
<metal:sortIcons use-macro="here/skyn/ref/macros/sortIcons" />
</tal:otherHeader>
</tal:showHeader>
</th>
<th tal:content="python: tool.translate('ref_actions')"></th>
@ -242,7 +264,7 @@
</table>
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment>
<metal:nav use-macro="here/skyn/macros/macros/appyNavigate"/>
<metal:nav use-macro="here/skyn/navigate/macros/appyNavigate"/>
</fieldset>
<tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment>

View file

@ -123,13 +123,17 @@ class AbstractWrapper:
# Update the ordered list of references
self.o._appy_getSortedField(fieldName).append(obj.UID())
def sort(self, fieldName):
'''Sorts referred elements linked to p_self via p_fieldName. At
present, it can only sort elements based on their title.'''
def sort(self, fieldName, sortKey='title', reverse=False):
'''Sorts referred elements linked to p_self via p_fieldName according
to a given p_sortKey which must be an attribute set on referred
objects ("title", by default).'''
sortedUids = getattr(self.o, '_appy_%s' % fieldName)
c = self.o.uid_catalog
sortedUids.sort(lambda x,y: \
cmp(c(UID=x)[0].getObject().Title(),c(UID=y)[0].getObject().Title()))
cmp(getattr(c(UID=x)[0].getObject().appy(), sortKey),
getattr(c(UID=y)[0].getObject().appy(), sortKey)))
if reverse:
sortedUids.reverse()
def create(self, fieldNameOrClass, **kwargs):
'''If p_fieldNameOfClass is the name of a field, this method allows to

View file

@ -10,11 +10,25 @@ class Descr:
class FieldDescr(Descr):
def __init__(self, atField, appyType, fieldRel):
self.atField = atField # The corresponding Archetypes field (may be None
# in the case of backward references)
self.appyType = appyType # The corresponding Appy type
self.fieldRel = fieldRel # The field relatonship, needed when the field
# description is a backward reference.
# The corresponding Archetypes field (may be None in the case of
# backward references)
self.atField = atField
# The corresponding Appy type
self.appyType = appyType
# The field relationship, needed when the field description is a
# backward reference.
self.fieldRel = fieldRel
# Can we sort this field ?
at = self.appyType
self.sortable = False
if not fieldRel and ((self.atField.getName() == 'title') or \
(at['indexed'])):
self.sortable = True
# Can we filter this field?
self.filterable = False
if not fieldRel and at['indexed'] and (at['type'] == 'String') and \
(at['format'] == 0) and not at['isSelect']:
self.filterable = True
if fieldRel:
self.widgetType = 'backField'
self.group = appyType['backd']['group']
@ -25,6 +39,7 @@ class FieldDescr(Descr):
self.group = appyType['group']
self.show = appyType['show']
self.page = appyType['page']
fieldName = self.atField.getName()
class GroupDescr(Descr):
def __init__(self, name, cols, page):

View file

@ -5,12 +5,12 @@ A POD template is a standard ODT file, where:
a portion of the document zero, one or more times ("if" and "for" statements);
- text insertions in "track changes" mode are interpreted as Python expressions.
When you invoke the tester.py program with one of those ODT files as unique parameter
(ie "python tester.py ForCellOnlyOne.odt"), you get a result.odt file which is the
When you run the Tester.py program with one of those ODT files as unique parameter
(ie "python Tester.py ForCellOnlyOne.odt"), you get a result.odt file which is the
result of executing the template with a bunch of Python objects. The "tests" dictionary
defined in tester.py contains the objects that are given to each POD ODT template
defined in Tester.py contains the objects that are given to each POD ODT template
contained in this folder.
Opening the templates with OpenOffice (2.0 or higher), running tester.py on it and
Opening the templates with OpenOffice (2.0 or higher), running Tester.py on it and
checking the result in result.odt is probably the quickest way to have a good idea
of what appy.pod can make for you !

File diff suppressed because it is too large Load diff