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 import re, time
from appy.shared.utils import Traceback 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 from appy.shared.data import countries
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
@ -54,6 +54,56 @@ class Search:
self.limit = limit self.limit = limit
self.fields = fields # This is a dict whose keys are indexed field self.fields = fields # This is a dict whose keys are indexed field
# names and whose values are search values. # 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: class Type:

View file

@ -105,10 +105,11 @@ class ToolMixin(AbstractMixin):
if res: return res[0].getObject() if res: return res[0].getObject()
return None return None
_sortFields = {'title': 'sortable_title'}
def executeQuery(self, contentType, flavourNumber=1, searchName=None, def executeQuery(self, contentType, flavourNumber=1, searchName=None,
startNumber=0, search=None, remember=False, 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 '''Executes a query on a given p_contentType (or several, separated
with commas) in Plone's portal_catalog. Portal types are from the with commas) in Plone's portal_catalog. Portal types are from the
flavour numbered p_flavourNumber. If p_searchName is specified, it flavour numbered p_flavourNumber. If p_searchName is specified, it
@ -136,7 +137,16 @@ class ToolMixin(AbstractMixin):
p_maxResults equals string "NO_LIMIT". p_maxResults equals string "NO_LIMIT".
If p_noSecurity is True, it gets all the objects, even those that the 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 ? # Is there one or several content types ?
if contentType.find(',') != -1: if contentType.find(',') != -1:
# Several content types are specified # Several content types are specified
@ -149,10 +159,9 @@ class ToolMixin(AbstractMixin):
params = {'portal_type': portalTypes} params = {'portal_type': portalTypes}
if not brainsOnly: params['batch'] = True if not brainsOnly: params['batch'] = True
# Manage additional criteria from a search when relevant # Manage additional criteria from a search when relevant
if searchName or search: if searchName:
# In this case, contentType must contain a single content type. # In this case, contentType must contain a single content type.
appyClass = self.getAppyClass(contentType) appyClass = self.getAppyClass(contentType)
if searchName:
if searchName != '_advanced': if searchName != '_advanced':
search = ArchetypesClassDescriptor.getSearch( search = ArchetypesClassDescriptor.getSearch(
appyClass, searchName) appyClass, searchName)
@ -164,44 +173,26 @@ class ToolMixin(AbstractMixin):
for fieldName, fieldValue in search.fields.iteritems(): for fieldName, fieldValue in search.fields.iteritems():
# Make the correspondance between the name of the field and the # Make the correspondance between the name of the field and the
# name of the corresponding index. # name of the corresponding index.
attrName = fieldName attrName = Search.getIndexName(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:])
# Express the field value in the way needed by the index # Express the field value in the way needed by the index
if isinstance(fieldValue, basestring) and \ params[attrName] = Search.getSearchValue(fieldName, fieldValue)
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
# Add a sort order if specified # Add a sort order if specified
sb = search.sortBy sortKey = search.sortBy
if sb: if sortKey:
# For field 'title', Plone has created a specific index params['sort_on'] = Search.getIndexName(sortKey, usage='sort')
# 'sortable_title', because index 'Title' is a ZCTextIndex # Determine or override sort if specified.
# (for searchability) and can't be used for sorting. if sortBy:
if self._sortFields.has_key(sb): sb = self._sortFields[sb] params['sort_on'] = Search.getIndexName(sortBy, usage='sort')
params['sort_on'] = sb 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 # Determine what method to call on the portal catalog
if noSecurity: catalogMethod = 'unrestrictedSearchResults' if noSecurity: catalogMethod = 'unrestrictedSearchResults'
else: catalogMethod = 'searchResults' else: catalogMethod = 'searchResults'
@ -544,14 +535,13 @@ class ToolMixin(AbstractMixin):
if cookieValue: return cookieValue.value if cookieValue: return cookieValue.value
return default return default
def getQueryUrl(self, contentType, flavourNumber, searchName, ajax=True, def getQueryUrl(self, contentType, flavourNumber, searchName,
startNumber=None): 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 request for getting queried objects from a search named p_searchName
on p_contentType from flavour numbered p_flavourNumber. If p_ajax on p_contentType from flavour numbered p_flavourNumber.'''
is False, it returns the non-ajax URL.'''
baseUrl = self.getAppFolder().absolute_url() + '/skyn' baseUrl = self.getAppFolder().absolute_url() + '/skyn'
baseParams = 'type_name=%s&flavourNumber=%s'%(contentType,flavourNumber) baseParams= 'type_name=%s&flavourNumber=%s' %(contentType,flavourNumber)
# Manage start number # Manage start number
rq = self.REQUEST rq = self.REQUEST
if startNumber != None: if startNumber != None:
@ -559,11 +549,7 @@ class ToolMixin(AbstractMixin):
elif rq.has_key('startNumber'): elif rq.has_key('startNumber'):
baseParams += '&startNumber=%s' % rq['startNumber'] baseParams += '&startNumber=%s' % rq['startNumber']
# Manage search name # Manage search name
if searchName or ajax: baseParams += '&search=%s' % searchName if searchName: baseParams += '&search=%s' % searchName
if ajax:
return '%s/ajax?objectUid=%s&page=macros&macro=queryResult&%s' % \
(baseUrl, self.UID(), baseParams)
else:
return '%s/query?%s' % (baseUrl, baseParams) return '%s/query?%s' % (baseUrl, baseParams)
def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize): def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize):
@ -666,7 +652,7 @@ class ToolMixin(AbstractMixin):
startNumber = self.computeStartNumberFrom(res['currentNumber']-1, startNumber = self.computeStartNumberFrom(res['currentNumber']-1,
res['totalNumber'], batchSize) res['totalNumber'], batchSize)
res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber, res['sourceUrl'] = self.getQueryUrl(contentType, flavourNumber,
searchName, ajax=False, startNumber=startNumber) searchName, startNumber=startNumber)
# Compute URLs # Compute URLs
for urlType in ('previous', 'next', 'first', 'last'): for urlType in ('previous', 'next', 'first', 'last'):
exec 'needIt = %sNeeded' % urlType exec 'needIt = %sNeeded' % urlType

View file

@ -659,6 +659,15 @@ class AbstractMixin:
isDelta = True isDelta = True
self.changeRefOrder(rq['fieldName'], rq['refObjectUid'], move, isDelta) 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): def getWorkflow(self, appy=True):
'''Returns the Appy workflow instance that is relevant for this '''Returns the Appy workflow instance that is relevant for this
object. If p_appy is False, it returns the DC workflow.''' 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(), exec 'self.set%s%s([])' % (fieldName[0].upper(),
fieldName[1:]) fieldName[1:])
def getUrl(self, t='view', **kwargs): def getUrl(self):
'''This method returns various URLs about this object.''' '''Returns the Appy URL for viewing this object.'''
baseUrl = self.absolute_url() return self.absolute_url() + '/skyn/view'
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 translate(self, label, mapping={}, domain=None, default=None): def translate(self, label, mapping={}, domain=None, default=None):
'''Translates a given p_label into p_domain with p_mapping.''' '''Translates a given p_label into p_domain with p_mapping.'''

View file

@ -1,15 +1,12 @@
<tal:comment replace="nothing"> <tal:comment replace="nothing">
This page is called by a XmlHttpRequest object. It requires parameters "page" and "macro": 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. 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 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. contextObj before returning the result of the macro to the browser.
</tal:comment> </tal:comment>
<tal:ajax define="page request/page; <tal:ajax define="page request/page;
macro request/macro; macro request/macro;
macroPath python: 'here/%s/macros/%s' % (page, macro); contextObj context/getParentNode;
contextObj python: context.uid_catalog(UID=request['objectUid'])[0].getObject();
action request/action|nothing; action request/action|nothing;
response request/RESPONSE; response request/RESPONSE;
member context/portal_membership/getAuthenticatedMember; member context/portal_membership/getAuthenticatedMember;
@ -18,8 +15,11 @@
dummy python:response.setHeader('Content-Type','text/html;;charset=utf-8'); dummy python:response.setHeader('Content-Type','text/html;;charset=utf-8');
dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT'); dummy2 python:response.setHeader('Expires', 'Mon, 11 Dec 1975 12:05:05 GMT');
dummy3 python:response.setHeader('CacheControl', 'no-cache')"> 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:executeAction condition="action">
<tal:do define="dummy python: contextObj.getAppyValue('on'+action)()" omit-tag=""/> <tal:do define="dummy python: contextObj.getAppyValue('on'+action)()" omit-tag=""/>
</tal:executeAction> </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> </tal:ajax>

View file

@ -295,12 +295,12 @@
batchSize historyInfo/batchSize; batchSize historyInfo/batchSize;
totalNumber historyInfo/totalNumber; totalNumber historyInfo/totalNumber;
ajaxHookId python:'appyHistory'; ajaxHookId python:'appyHistory';
baseUrl python: contextObj.getUrl('showHistory', startNumber='**v**'); navBaseCall python: 'askObjectHistory(\'%s\',\'%s\',**v**)' % (ajaxHookId, contextObj.absolute_url());
tool contextObj/getTool"> tool contextObj/getTool">
<tal:comment replace="nothing">Table containing the history</tal:comment> <tal:comment replace="nothing">Table containing the history</tal:comment>
<tal:history condition="objs"> <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"> <table width="100%" class="listing nosort">
<tr i18n:domain="plone"> <tr i18n:domain="plone">
<th i18n:translate="listingheader_action"/> <th i18n:translate="listingheader_action"/>
@ -370,8 +370,11 @@
this.xhr = false; this.xhr = false;
if (window.XMLHttpRequest) this.xhr = new XMLHttpRequest(); if (window.XMLHttpRequest) this.xhr = new XMLHttpRequest();
else this.xhr = new ActiveXObject("Microsoft.XMLHTTP"); else this.xhr = new ActiveXObject("Microsoft.XMLHTTP");
this.hook = ''; // The ID of the HTML element in the page that will be this.hook = ''; /* The ID of the HTML element in the page that will be
// replaced by result of executing the Ajax request. 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) { function getAjaxChunk(pos) {
@ -391,15 +394,35 @@
var hookElem = document.getElementById(hook); var hookElem = document.getElementById(hook);
if (hookElem && (xhrObjects[pos].xhr.status == 200)) { if (hookElem && (xhrObjects[pos].xhr.status == 200)) {
hookElem.innerHTML = xhrObjects[pos].xhr.responseText; 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; xhrObjects[pos].freed = 1;
} }
} }
} }
function askAjaxChunk(hook, url) { function askAjaxChunk(hook,mode,url,page,macro,params,beforeSend,onGet) {
// This function will ask to get a chunk of HTML on the server by /* This function will ask to get a chunk of HTML on the server through a
// triggering a XMLHttpRequest. 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. // First, get a non-busy XMLHttpRequest object.
var pos = -1; var pos = -1;
for (var i=0; i < xhrObjects.length; i++) { for (var i=0; i < xhrObjects.length; i++) {
@ -410,14 +433,84 @@
xhrObjects[pos] = new XhrObject(); xhrObjects[pos] = new XhrObject();
} }
xhrObjects[pos].hook = hook; xhrObjects[pos].hook = hook;
xhrObjects[pos].onGet = onGet;
if (xhrObjects[pos].xhr) { if (xhrObjects[pos].xhr) {
xhrObjects[pos].freed = 0; var rq = xhrObjects[pos];
// Perform the asynchronous HTTP GET rq.freed = 0;
xhrObjects[pos].xhr.open('GET', url, true); // Construct parameters
xhrObjects[pos].xhr.onreadystatechange = function() { getAjaxChunk(pos); } var paramsFull = 'page=' + page + '&macro=' + macro;
if (window.XMLHttpRequest) { xhrObjects[pos].xhr.send(null); } if (params) {
else if (window.ActiveXObject) { xhrObjects[pos].xhr.send(); } 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 // Function used by checkbox widgets for having radio-button-like behaviour
@ -633,10 +726,9 @@
<td colspan="2"> <td colspan="2">
<span id="appyHistory" <span id="appyHistory"
tal:attributes="style python:test(historyExpanded, 'display:block', 'display:none')"> tal:attributes="style python:test(historyExpanded, 'display:block', 'display:none')">
<div tal:define="ajaxHookId python: contextObj.UID() + '_history'; <div tal:define="ajaxHookId python: contextObj.UID() + '_history';"
ajaxUrl python: contextObj.getUrl('showHistory')"
tal:attributes="id ajaxHookId"> 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> </script>
</div> </div>
</span> </span>
@ -655,7 +747,7 @@
</td> </td>
</tr> </tr>
</table> </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> <tal:comment replace="nothing">Tabs</tal:comment>
<ul class="contentViews appyTabs" tal:condition="python: len(appyPages)&gt;1"> <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)); searchLabel python: test(searchName=='_advanced', 'search_results', '%s_search_%s' % (contentType, searchName));
searchDescr python: '%s_descr' % searchLabel; searchDescr python: '%s_descr' % searchLabel;
severalTypes python: contentType and (contentType.find(',') != -1); 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; objs queryResult/objects;
totalNumber queryResult/totalNumber; totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize; batchSize queryResult/batchSize;
ajaxHookId python:'queryResult'; 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);"> newSearchUrl python: '%s/skyn/search?type_name=%s&flavourNumber=%d' % (tool.getAppFolder().absolute_url(), contentType, flavourNumber);">
<tal:result condition="objs"> <tal:result condition="objs">
@ -753,7 +849,7 @@
</td> </td>
<td align="right" width="25%"> <td align="right" width="25%">
<tal:comment replace="nothing">Appy (top) navigation</tal:comment> <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> </td>
</tr></table> </tr></table>
@ -763,26 +859,20 @@
excepted for workflow state (which is not a field): in this case it is simply the excepted for workflow state (which is not a field): in this case it is simply the
string "workflowState".</tal:comment> 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> <tr>
<tal:comment replace="nothing">Mandatory column "Title"/"Name"</tal:comment> <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\')';" <th tal:define="fieldName python:'title'; sortable python:True; filterable python:True">
id="arrow_title" style="cursor:pointer"/-->
<span tal:content="python: tool.translate('ref_name')"/> <span tal:content="python: tool.translate('ref_name')"/>
<!--input id="filter_title" type="text" size="5" onkeyup="javascript:onTextEntered('title')"/--> <metal:sortAndFilter use-macro="here/skyn/navigate/macros/sortAndFilter"/>
<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>
</th> </th>
<tal:comment replace="nothing">Columns corresponding to other fields</tal:comment> <tal:comment replace="nothing">Columns corresponding to other fields</tal:comment>
<tal:columnHeader repeat="fieldDescr fieldDescrs"> <tal:columnHeader repeat="fieldDescr fieldDescrs">
<th tal:define="fieldName fieldDescr/atField/getName|string:workflow_state"> <th tal:define="fieldName fieldDescr/atField/getName|string:workflow_state;
<!--img tal:attributes= "src string: $portal_url/arrowDown.gif; sortable fieldDescr/sortable|nothing;
onClick python:'javascript:onSort(\'%s\')' % fieldName; filterable fieldDescr/filterable|nothing;">
id python: 'arrow_%s' % fieldName"
style="cursor:pointer"/-->
<tal:comment replace="nothing">Display header for a "standard" field</tal:comment> <tal:comment replace="nothing">Display header for a "standard" field</tal:comment>
<tal:standardField condition="python: fieldName != 'workflow_state'"> <tal:standardField condition="python: fieldName != 'workflow_state'">
<span tal:replace="python: tool.translate(fieldDescr['atField'].widget.label_msgid)"/> <span tal:replace="python: tool.translate(fieldDescr['atField'].widget.label_msgid)"/>
@ -791,20 +881,13 @@
<tal:workflowState condition="python: fieldName == 'workflow_state'"> <tal:workflowState condition="python: fieldName == 'workflow_state'">
<span tal:replace="python: tool.translate('workflow_state')"/> <span tal:replace="python: tool.translate('workflow_state')"/>
</tal:workflowState> </tal:workflowState>
<!--input type="text" size="5" <metal:sortAndFilter use-macro="here/skyn/navigate/macros/sortAndFilter"/>
tal:attributes="id python: 'filter_%s' % fieldName;
onkeyup python:'javascript:onTextEntered(\'%s\')' % fieldName"/-->
</th> </th>
</tal:columnHeader> </tal:columnHeader>
<tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment> <tal:comment replace="nothing">Column "Object type", shown if instances of several types are shown</tal:comment>
<th tal:condition="severalTypes"><!--img <th tal:condition="severalTypes">
tal:attributes= "src string: $portal_url/arrowDown.gif;
onClick python:'javascript:onSort(\'root_type\')';"
id = "arrow_root_type" style="cursor:pointer"/-->
<span tal:replace="python: tool.translate('root_type')"/> <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> </th>
<tal:comment replace="nothing">Column "Actions"</tal:comment> <tal:comment replace="nothing">Column "Actions"</tal:comment>
@ -872,7 +955,7 @@
</table> </table>
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment> <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> </fieldset>
</tal:result> </tal:result>
@ -952,91 +1035,3 @@
</table> </table>
</form> </form>
</metal:transitions> </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', '')"> searchName python:context.REQUEST.get('search', '')">
<div metal:use-macro="here/skyn/macros/macros/pagePrologue"/> <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> <tal:comment replace="nothing">Query result</tal:comment>
<div id="queryResult"></div> <div id="queryResult"></div>
<script language="javascript" <script language="javascript"
tal:define="ajaxUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName)" 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> </script>
</div> </div>
</body> </body>

View file

@ -20,19 +20,19 @@
<tr> <tr>
<tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment> <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)"> <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); <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> <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; tal:attributes="src string: $portal_url/skyn/arrowUp.png;
title python: tool.translate('move_up'); title python: tool.translate('move_up');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, ajaxUrl)" onClick python: ajaxBaseCall.replace('**v**', 'up')"
style="cursor:pointer"/> style="cursor:pointer"/>
<tal:comment replace="nothing">Move down</tal:comment> <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; tal:attributes="src string: $portal_url/skyn/arrowDown.png;
title python: tool.translate('move_down'); title python: tool.translate('move_down');
onClick python: 'askAjaxChunk(\'%s\', \'%s\')' % (ajaxHookId, ajaxUrl)" onClick python: ajaxBaseCall.replace('**v**', 'down')"
style="cursor:pointer"/> style="cursor:pointer"/>
</tal:moveRef> </tal:moveRef>
</td> </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)"/> 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> </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"> <tal:comment replace="nothing">
This macro shows a reference field. More precisely, it shows nothing, but calls 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 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 - descrId (string) the i18n id of the reference field description
</tal:comment> </tal:comment>
<div metal:define-macro="showReference" <div metal:define-macro="showReference"
tal:define="ajaxHookId python: contextObj.UID() + fieldName; tal:define="ajaxHookId python: contextObj.UID() + fieldName"
ajaxUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId})"
tal:attributes="id ajaxHookId"> tal:attributes="id ajaxHookId">
<script language="javascript" <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> </script>
</div> </div>
@ -113,7 +128,7 @@
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1); atMostOneRef python: (multiplicity[1] == 1) and (len(objs)&lt;=1);
label python: tool.translate(labelId); label python: tool.translate(labelId);
description python: tool.translate(descrId); 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. <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:content="description" class="discreet" ></p>
<tal:comment replace="nothing">Appy (top) navigation</tal:comment> <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> <tal:comment replace="nothing">No object is present</tal:comment>
<p tal:condition="not:objs" tal:content="python: tool.translate('no_ref')"></p> <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"> align="right" tal:condition="python: not isBack and objs" cellpadding="0" cellspacing="0">
<tr tal:condition="appyType/showHeaders"> <tr tal:condition="appyType/showHeaders">
<th tal:condition="python: 'title' not in appyType['shownInfo']" <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"> <th tal:repeat="shownField appyType/shownInfo">
<tal:showHeader condition="python: objs[0].getField(shownField)"> <tal:showHeader condition="python: objs[0].getField(shownField)">
<tal:titleHeader condition="python: shownField == 'title'" <tal:titleHeader condition="python: shownField == 'title'">
content="python: tool.translate('ref_name')"/> <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'" <tal:otherHeader condition="python: shownField != 'title'"
define="labelId python: objs[0].getField(shownField).widget.label_msgid" define="labelId python: objs[0].getField(shownField).widget.label_msgid">
content="python: tool.translate(labelId)"/> <span tal:content="python: tool.translate(labelId)"></span>
<metal:sortIcons use-macro="here/skyn/ref/macros/sortIcons" />
</tal:otherHeader>
</tal:showHeader> </tal:showHeader>
</th> </th>
<th tal:content="python: tool.translate('ref_actions')"></th> <th tal:content="python: tool.translate('ref_actions')"></th>
@ -242,7 +264,7 @@
</table> </table>
<tal:comment replace="nothing">Appy (bottom) navigation</tal:comment> <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> </fieldset>
<tal:comment replace="nothing">A carriage return needed in some cases.</tal:comment> <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 # Update the ordered list of references
self.o._appy_getSortedField(fieldName).append(obj.UID()) self.o._appy_getSortedField(fieldName).append(obj.UID())
def sort(self, fieldName): def sort(self, fieldName, sortKey='title', reverse=False):
'''Sorts referred elements linked to p_self via p_fieldName. At '''Sorts referred elements linked to p_self via p_fieldName according
present, it can only sort elements based on their title.''' to a given p_sortKey which must be an attribute set on referred
objects ("title", by default).'''
sortedUids = getattr(self.o, '_appy_%s' % fieldName) sortedUids = getattr(self.o, '_appy_%s' % fieldName)
c = self.o.uid_catalog c = self.o.uid_catalog
sortedUids.sort(lambda x,y: \ 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): def create(self, fieldNameOrClass, **kwargs):
'''If p_fieldNameOfClass is the name of a field, this method allows to '''If p_fieldNameOfClass is the name of a field, this method allows to

View file

@ -10,11 +10,25 @@ class Descr:
class FieldDescr(Descr): class FieldDescr(Descr):
def __init__(self, atField, appyType, fieldRel): def __init__(self, atField, appyType, fieldRel):
self.atField = atField # The corresponding Archetypes field (may be None # The corresponding Archetypes field (may be None in the case of
# in the case of backward references) # backward references)
self.appyType = appyType # The corresponding Appy type self.atField = atField
self.fieldRel = fieldRel # The field relatonship, needed when the field # The corresponding Appy type
# description is a backward reference. 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: if fieldRel:
self.widgetType = 'backField' self.widgetType = 'backField'
self.group = appyType['backd']['group'] self.group = appyType['backd']['group']
@ -25,6 +39,7 @@ class FieldDescr(Descr):
self.group = appyType['group'] self.group = appyType['group']
self.show = appyType['show'] self.show = appyType['show']
self.page = appyType['page'] self.page = appyType['page']
fieldName = self.atField.getName()
class GroupDescr(Descr): class GroupDescr(Descr):
def __init__(self, name, cols, page): 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); 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. - 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 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 (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 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. 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 checking the result in result.odt is probably the quickest way to have a good idea
of what appy.pod can make for you ! of what appy.pod can make for you !

File diff suppressed because it is too large Load diff