Reworked AJAX framework with a lot of new sorting and filtering possibilities.
This commit is contained in:
parent
46cda3f755
commit
fd775e17a2
|
@ -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:
|
||||||
|
|
|
@ -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,59 +159,40 @@ 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)
|
else:
|
||||||
else:
|
fields = self.REQUEST.SESSION['searchCriteria']
|
||||||
fields = self.REQUEST.SESSION['searchCriteria']
|
search = Search('customSearch', **fields)
|
||||||
search = Search('customSearch', **fields)
|
|
||||||
if search:
|
if search:
|
||||||
# Add additional search criteria
|
# Add additional search criteria
|
||||||
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,12 +549,8 @@ 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/query?%s' % (baseUrl, baseParams)
|
||||||
return '%s/ajax?objectUid=%s&page=macros¯o=queryResult&%s' % \
|
|
||||||
(baseUrl, self.UID(), baseParams)
|
|
||||||
else:
|
|
||||||
return '%s/query?%s' % (baseUrl, baseParams)
|
|
||||||
|
|
||||||
def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize):
|
def computeStartNumberFrom(self, currentNumber, totalNumber, batchSize):
|
||||||
'''Returns the number (start at 0) of the first element in a list
|
'''Returns the number (start at 0) of the first element in a list
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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¯o=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.'''
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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: "¶m1=blabla¶m2=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,16 +433,86 @@
|
||||||
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 + '¯o=' + 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
|
||||||
function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
|
function toggleCheckbox(visibleCheckbox, hiddenBoolean) {
|
||||||
vis = document.getElementById(visibleCheckbox);
|
vis = document.getElementById(visibleCheckbox);
|
||||||
|
@ -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)>1">
|
<ul class="contentViews appyTabs" tal:condition="python: len(appyPages)>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 > 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">
|
|
||||||
<span tal:replace="python: startNumber+1"/>
|
|
||||||
<img tal:attributes="src string: $portal_url/skyn/to.png"/>
|
|
||||||
<span tal:replace="python: startNumber+len(objs)"/> <b>//</b>
|
|
||||||
<span tal:replace="python: totalNumber"/>
|
|
||||||
</td>
|
|
||||||
<tal:comment replace="nothing">Go to the next page</tal:comment>
|
|
||||||
<td><img style="cursor:pointer" tal:condition="python: sNumber < 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">
|
|
||||||
<span tal:replace="python: currentNumber"/> <b>//</b>
|
|
||||||
<span tal:replace="python: totalNumber"/>
|
|
||||||
</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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)>1) and member.has_permission('Modify portal content', contextObj)">
|
<td class="noPadding" tal:condition="python: (len(objs)>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 > 0"
|
<img tal:condition="python: objectIndex > 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 < (totalNumber-1)"
|
<img tal:condition="python: objectIndex < (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)<=1);
|
atMostOneRef python: (multiplicity[1] == 1) and (len(objs)<=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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
25
gen/utils.py
25
gen/utils.py
|
@ -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):
|
||||||
|
|
|
@ -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 !
|
||||||
|
|
1062
pod/test/Tests.rtf
1062
pod/test/Tests.rtf
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue