[gen] Class.listColumns can now be a static method (accepting the tool as single arg). [gen] Indexed Ref fields are now sortable. For every such field, 2 indexes are created: a list index for searching, and a standard index for sorting (by their title).
This commit is contained in:
parent
5c41f1b3d2
commit
c11002b7d5
|
@ -325,6 +325,7 @@ class Field:
|
||||||
'''Can fields of this type be used for sorting purposes (when sorting
|
'''Can fields of this type be used for sorting purposes (when sorting
|
||||||
search results (p_usage="search") or when sorting reference fields
|
search results (p_usage="search") or when sorting reference fields
|
||||||
(p_usage="ref")?'''
|
(p_usage="ref")?'''
|
||||||
|
if self.name == 'state': return
|
||||||
if usage == 'search':
|
if usage == 'search':
|
||||||
return self.indexed and not self.isMultiValued() and not \
|
return self.indexed and not self.isMultiValued() and not \
|
||||||
((self.type == 'String') and self.isSelection())
|
((self.type == 'String') and self.isSelection())
|
||||||
|
@ -576,7 +577,7 @@ class Field:
|
||||||
If p_forSearch is True, it will return a "string" version of the
|
If p_forSearch is True, it will return a "string" version of the
|
||||||
index value suitable for a global search.'''
|
index value suitable for a global search.'''
|
||||||
res = self.getValue(obj)
|
res = self.getValue(obj)
|
||||||
# Zope catalog does not like unicode strings.
|
# Zope catalog does not like unicode strings
|
||||||
if isinstance(res, unicode): res = res.encode('utf-8')
|
if isinstance(res, unicode): res = res.encode('utf-8')
|
||||||
if forSearch and (res != None):
|
if forSearch and (res != None):
|
||||||
if type(res) in sutils.sequenceTypes:
|
if type(res) in sutils.sequenceTypes:
|
||||||
|
@ -589,6 +590,29 @@ class Field:
|
||||||
res = str(res)
|
res = str(res)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def getIndexName(self, usage='search'):
|
||||||
|
'''Gets the name of the Zope index that corresponds to this field.
|
||||||
|
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.'''
|
||||||
|
# Manage special cases
|
||||||
|
if self.name == 'title':
|
||||||
|
# For field 'title', Appy has a specific index 'SortableTitle',
|
||||||
|
# because index 'Title' is a TextIndex (for searchability) and can't
|
||||||
|
# be used for sorting.
|
||||||
|
return (usage == 'sort') and 'SortableTitle' or 'Title'
|
||||||
|
elif self.name == 'state': return 'State'
|
||||||
|
else:
|
||||||
|
res = 'get%s%s'% (self.name[0].upper(), self.name[1:])
|
||||||
|
if (usage == 'sort') and self.hasSortIndex(): res += '_sort'
|
||||||
|
return res
|
||||||
|
|
||||||
|
def hasSortIndex(self):
|
||||||
|
'''Some fields have indexes that prevents sorting (ie, list indexes).
|
||||||
|
Those fields may define a secondary index, specifically for sorting.
|
||||||
|
This is the case of Ref fields for example.'''
|
||||||
|
return
|
||||||
|
|
||||||
def getCatalogValue(self, obj, usage='search'):
|
def getCatalogValue(self, obj, usage='search'):
|
||||||
'''This method returns the index value that is currently stored in the
|
'''This method returns the index value that is currently stored in the
|
||||||
catalog for this field on p_obj.'''
|
catalog for this field on p_obj.'''
|
||||||
|
@ -596,7 +620,7 @@ class Field:
|
||||||
raise Exception('Field %s: cannot retrieve catalog version of ' \
|
raise Exception('Field %s: cannot retrieve catalog version of ' \
|
||||||
'unindexed field.' % self.name)
|
'unindexed field.' % self.name)
|
||||||
tool = obj.getTool()
|
tool = obj.getTool()
|
||||||
indexName = Search.getIndexName(self.name, usage=usage)
|
indexName = self.getIndexName(usage)
|
||||||
catalogBrain = tool.getObject(obj.id, brain=True)
|
catalogBrain = tool.getObject(obj.id, brain=True)
|
||||||
index = tool.getApp().catalog.Indexes[indexName]
|
index = tool.getApp().catalog.Indexes[indexName]
|
||||||
return index.getEntryForObject(catalogBrain.getRID())
|
return index.getEntryForObject(catalogBrain.getRID())
|
||||||
|
|
|
@ -182,7 +182,7 @@ class Ref(Field):
|
||||||
# ref field according to the field that corresponds to this column.
|
# ref field according to the field that corresponds to this column.
|
||||||
pxSortIcons = Px('''
|
pxSortIcons = Px('''
|
||||||
<x if="changeOrder and (len(objects) > 1) and \
|
<x if="changeOrder and (len(objects) > 1) and \
|
||||||
ztool.isSortable(refField.name, tiedClassName, 'ref')"
|
refField.isSortable(usage='ref')"
|
||||||
var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \
|
var2="ajaxBaseCall=navBaseCall.replace('**v**', '%s,%s,{%s:%s,%s:%s}'% \
|
||||||
(q(startNumber), q('sort'), q('sortKey'), q(refField.name), \
|
(q(startNumber), q('sort'), q('sortKey'), q(refField.name), \
|
||||||
q('reverse'), q('**v**')))">
|
q('reverse'), q('**v**')))">
|
||||||
|
@ -963,10 +963,14 @@ class Ref(Field):
|
||||||
res = ['']
|
res = ['']
|
||||||
return res
|
return res
|
||||||
else:
|
else:
|
||||||
# For the global search: return linked objects' titles.
|
# For the global search: return linked objects' titles
|
||||||
res = [o.title for o in self.getValue()]
|
return ' '.join([o.getShownValue('title') \
|
||||||
if not res: res.append('')
|
for o in self.getValue(obj, appy=False)])
|
||||||
return res
|
|
||||||
|
def hasSortIndex(self):
|
||||||
|
'''An indexed Ref field is of type "ListIndex", which is not sortable.
|
||||||
|
So an additional FieldIndex is required.'''
|
||||||
|
return True
|
||||||
|
|
||||||
def validateValue(self, obj, value):
|
def validateValue(self, obj, value):
|
||||||
if not self.link: return
|
if not self.link: return
|
||||||
|
|
|
@ -55,23 +55,19 @@ class Search:
|
||||||
self.checkboxesDefault = checkboxesDefault
|
self.checkboxesDefault = checkboxesDefault
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getIndexName(fieldName, usage='search'):
|
def getIndexName(name, klass, usage='search'):
|
||||||
'''Gets the name of the technical index that corresponds to field named
|
'''Gets the name of the Zope index that corresponds to p_name. Indexes
|
||||||
p_fieldName. Indexes can be used for searching (p_usage="search") or
|
can be used for searching (p_usage="search") or for sorting
|
||||||
for sorting (usage="sort"). The method returns None if the field
|
(usage="sort"). The method returns None if the field named
|
||||||
named p_fieldName can't be used for p_usage.'''
|
p_name can't be used for p_usage.'''
|
||||||
if fieldName == 'title':
|
# Manage indexes that do not have a corresponding field
|
||||||
if usage == 'search': return 'Title'
|
if name == 'created': return 'Created'
|
||||||
else: return 'SortableTitle'
|
elif name == 'modified': return 'Modified'
|
||||||
# Indeed, for field 'title', Appy has a specific index
|
elif name in defaultIndexes: return name
|
||||||
# 'SortableTitle', because index 'Title' is a TextIndex
|
|
||||||
# (for searchability) and can't be used for sorting.
|
|
||||||
elif fieldName == 'state': return 'State'
|
|
||||||
elif fieldName == 'created': return 'Created'
|
|
||||||
elif fieldName == 'modified': return 'Modified'
|
|
||||||
elif fieldName in defaultIndexes: return fieldName
|
|
||||||
else:
|
else:
|
||||||
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
|
# Manage indexes corresponding to fields
|
||||||
|
field = getattr(klass, name, None)
|
||||||
|
if field: return field.getIndexName(usage)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getSearchValue(fieldName, fieldValue, klass):
|
def getSearchValue(fieldName, fieldValue, klass):
|
||||||
|
@ -116,30 +112,28 @@ class Search:
|
||||||
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
|
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
|
||||||
sort_order).'''
|
sort_order).'''
|
||||||
# Put search criteria in p_criteria
|
# Put search criteria in p_criteria
|
||||||
for fieldName, fieldValue in self.fields.iteritems():
|
for name, value in self.fields.iteritems():
|
||||||
# Management of searches restricted to objects linked through a
|
# Management of searches restricted to objects linked through a
|
||||||
# Ref field: not implemented yet.
|
# Ref field: not implemented yet.
|
||||||
if fieldName == '_ref': continue
|
if name == '_ref': continue
|
||||||
# Make the correspondence between the name of the field and the
|
# Make the correspondence between the name of the field and the
|
||||||
# name of the corresponding index, excepted if advanced is True: in
|
# name of the corresponding index, excepted if advanced is True: in
|
||||||
# that case, the correspondence will be done later.
|
# that case, the correspondence will be done later.
|
||||||
if not advanced:
|
if not advanced:
|
||||||
attrName = Search.getIndexName(fieldName)
|
indexName = Search.getIndexName(name, klass)
|
||||||
# Express the field value in the way needed by the index
|
# Express the field value in the way needed by the index
|
||||||
criteria[attrName] = Search.getSearchValue(fieldName,
|
criteria[indexName] = Search.getSearchValue(name, value, klass)
|
||||||
fieldValue, klass)
|
|
||||||
else:
|
else:
|
||||||
criteria[fieldName]= fieldValue
|
criteria[name] = value
|
||||||
# Add a sort order if specified
|
# Add a sort order if specified
|
||||||
if self.sortBy:
|
if self.sortBy:
|
||||||
|
c = criteria
|
||||||
if not advanced:
|
if not advanced:
|
||||||
criteria['sort_on'] = Search.getIndexName(self.sortBy,
|
c['sort_on']=Search.getIndexName(self.sortBy,klass,usage='sort')
|
||||||
usage='sort')
|
c['sort_order']= (self.sortOrder=='desc') and 'reverse' or None
|
||||||
if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse'
|
|
||||||
else: criteria['sort_order'] = None
|
|
||||||
else:
|
else:
|
||||||
criteria['sortBy'] = self.sortBy
|
c['sortBy'] = self.sortBy
|
||||||
criteria['sortOrder'] = self.sortOrder
|
c['sortOrder'] = self.sortOrder
|
||||||
|
|
||||||
def isShowable(self, klass, tool):
|
def isShowable(self, klass, tool):
|
||||||
'''Is this Search instance (defined in p_klass) showable?'''
|
'''Is this Search instance (defined in p_klass) showable?'''
|
||||||
|
|
|
@ -32,7 +32,7 @@ class ClassDescriptor(Descriptor):
|
||||||
self.name = getClassName(self.klass, generator.applicationName)
|
self.name = getClassName(self.klass, generator.applicationName)
|
||||||
self.predefined = False
|
self.predefined = False
|
||||||
self.customized = False
|
self.customized = False
|
||||||
# Phase and page names will be calculated later, when first required.
|
# Phase and page names will be calculated later, when first required
|
||||||
self.phases = None
|
self.phases = None
|
||||||
self.pages = None
|
self.pages = None
|
||||||
|
|
||||||
|
@ -206,19 +206,26 @@ class ClassDescriptor(Descriptor):
|
||||||
if search.name == searchName:
|
if search.name == searchName:
|
||||||
return search
|
return search
|
||||||
|
|
||||||
def addIndexMethod(self, field):
|
def addIndexMethod(self, field, secondary=False):
|
||||||
'''For indexed p_field, this method generates a method that allows to
|
'''For indexed p_field, this method generates a method that allows to
|
||||||
get the value of the field as must be copied into the corresponding
|
get the value of the field as must be copied into the corresponding
|
||||||
index.'''
|
index. Some fields have a secondary index for sorting purposes. If
|
||||||
|
p_secondary is True, this method generates the method for this
|
||||||
|
secondary index.'''
|
||||||
m = self.methods
|
m = self.methods
|
||||||
spaces = TABS
|
spaces = TABS
|
||||||
n = field.fieldName
|
n = field.fieldName
|
||||||
m += '\n' + ' '*spaces + 'def get%s%s(self):\n' % (n[0].upper(), n[1:])
|
suffix = secondary and '_sort' or ''
|
||||||
|
m += '\n' + ' '*spaces + 'def get%s%s%s(self):\n' % \
|
||||||
|
(n[0].upper(), n[1:], suffix)
|
||||||
spaces += TABS
|
spaces += TABS
|
||||||
m += ' '*spaces + "'''Gets indexable value of field \"%s\".'''\n" % n
|
m += ' '*spaces + "'''Gets indexable value of field \"%s\".'''\n" % n
|
||||||
|
suffix = secondary and ', True' or ''
|
||||||
m += ' '*spaces + 'return self.getAppyType("%s").getIndexValue(' \
|
m += ' '*spaces + 'return self.getAppyType("%s").getIndexValue(' \
|
||||||
'self)\n' % n
|
'self%s)\n' % (n, suffix)
|
||||||
self.methods = m
|
self.methods = m
|
||||||
|
if not secondary and field.appyType.hasSortIndex():
|
||||||
|
self.addIndexMethod(field, secondary=True)
|
||||||
|
|
||||||
def addField(self, fieldName, fieldType):
|
def addField(self, fieldName, fieldType):
|
||||||
'''Adds a new field to the Tool.'''
|
'''Adds a new field to the Tool.'''
|
||||||
|
@ -493,7 +500,7 @@ class TranslationClassDescriptor(ClassDescriptor):
|
||||||
mHeight = int(len(msgContent)/maxLine) + msgContent.count('<br/>')
|
mHeight = int(len(msgContent)/maxLine) + msgContent.count('<br/>')
|
||||||
height = max(height, mHeight)
|
height = max(height, mHeight)
|
||||||
if height < 1:
|
if height < 1:
|
||||||
# This is a one-line field.
|
# This is a one-line field
|
||||||
params['width'] = width
|
params['width'] = width
|
||||||
else:
|
else:
|
||||||
# This is a multi-line field, or a very-long-single-lined field
|
# This is a multi-line field, or a very-long-single-lined field
|
||||||
|
|
|
@ -334,22 +334,21 @@ class ToolMixin(BaseMixin):
|
||||||
If p_refObject and p_refField are given, the query is limited to the
|
If p_refObject and p_refField are given, the query is limited to the
|
||||||
objects that are referenced from p_refObject through p_refField.'''
|
objects that are referenced from p_refObject through p_refField.'''
|
||||||
params = {'ClassName': className}
|
params = {'ClassName': className}
|
||||||
appyClass = self.getAppyClass(className, wrapper=True)
|
klass = self.getAppyClass(className, wrapper=True)
|
||||||
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: search = self.getSearch(className, searchName)
|
if searchName: search = self.getSearch(className, searchName)
|
||||||
if search:
|
if search:
|
||||||
# Add in params search and sort criteria.
|
# Add in params search and sort criteria
|
||||||
search.updateSearchCriteria(params, appyClass)
|
search.updateSearchCriteria(params, klass)
|
||||||
# Determine or override sort if specified.
|
# Determine or override sort if specified
|
||||||
if sortBy:
|
if sortBy:
|
||||||
params['sort_on'] = Search.getIndexName(sortBy, usage='sort')
|
params['sort_on'] = Search.getIndexName(sortBy, klass, usage='sort')
|
||||||
if sortOrder == 'desc': params['sort_order'] = 'reverse'
|
params['sort_order'] = (sortOrder == 'desc') and 'reverse' or None
|
||||||
else: params['sort_order'] = None
|
# If defined, add the filter among search parameters
|
||||||
# If defined, add the filter among search parameters.
|
|
||||||
if filterKey:
|
if filterKey:
|
||||||
filterKey = Search.getIndexName(filterKey)
|
filterKey = Search.getIndexName(filterKey, klass)
|
||||||
filterValue = Search.getSearchValue(filterKey,filterValue,appyClass)
|
filterValue = Search.getSearchValue(filterKey, filterValue, klass)
|
||||||
params[filterKey] = filterValue
|
params[filterKey] = filterValue
|
||||||
# TODO This value needs to be merged with an existing one if already
|
# 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
|
# in params, or, in a first step, we should avoid to display the
|
||||||
|
@ -367,7 +366,7 @@ class ToolMixin(BaseMixin):
|
||||||
self.appy().numberOfResultsPerPage
|
self.appy().numberOfResultsPerPage
|
||||||
elif maxResults == 'NO_LIMIT':
|
elif maxResults == 'NO_LIMIT':
|
||||||
maxResults = None
|
maxResults = None
|
||||||
# Return brains only if required.
|
# Return brains only if required
|
||||||
if brainsOnly:
|
if brainsOnly:
|
||||||
if not maxResults: return brains
|
if not maxResults: return brains
|
||||||
else: return brains[:maxResults]
|
else: return brains[:maxResults]
|
||||||
|
@ -399,7 +398,9 @@ class ToolMixin(BaseMixin):
|
||||||
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
return refInfo[0].getAppyType(refInfo[1]).shownInfo
|
||||||
else:
|
else:
|
||||||
k = self.getAppyClass(className)
|
k = self.getAppyClass(className)
|
||||||
return hasattr(k, 'listColumns') and k.listColumns or ('title',)
|
if not hasattr(k, 'listColumns'): return ('title',)
|
||||||
|
if callable(k.listColumns): return k.listColumns(self.appy())
|
||||||
|
return k.listColumns
|
||||||
|
|
||||||
def truncateValue(self, value, width=20):
|
def truncateValue(self, value, width=20):
|
||||||
'''Truncates the p_value according to p_width. p_value has to be
|
'''Truncates the p_value according to p_width. p_value has to be
|
||||||
|
@ -510,13 +511,6 @@ class ToolMixin(BaseMixin):
|
||||||
if role in creators:
|
if role in creators:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def isSortable(self, name, className, usage):
|
|
||||||
'''Is field p_name defined on p_className sortable for p_usage purposes
|
|
||||||
(p_usage can be "ref" or "search")?'''
|
|
||||||
if (',' in className) or (name == 'state'): return False
|
|
||||||
appyType = self.getAppyType(name, className=className)
|
|
||||||
if appyType: return appyType.isSortable(usage=usage)
|
|
||||||
|
|
||||||
def subTitleIsUsed(self, className):
|
def subTitleIsUsed(self, className):
|
||||||
'''Does class named p_className define a method "getSubTitle"?'''
|
'''Does class named p_className define a method "getSubTitle"?'''
|
||||||
klass = self.getAppyClass(className)
|
klass = self.getAppyClass(className)
|
||||||
|
|
|
@ -102,7 +102,7 @@ td.search { padding-top: 8px }
|
||||||
border: 1px solid grey; box-shadow: 2px 2px 2px #888888}
|
border: 1px solid grey; box-shadow: 2px 2px 2px #888888}
|
||||||
.dropdown { display:none; position: absolute; top: 15px; left: 1px;
|
.dropdown { display:none; position: absolute; top: 15px; left: 1px;
|
||||||
border: 1px solid #cccccc; background-color: white;
|
border: 1px solid #cccccc; background-color: white;
|
||||||
padding: 3px 4px 0; font-size: 8pt; font-weight: normal;
|
padding: 3px 4px 3px; font-size: 8pt; font-weight: normal;
|
||||||
text-align: left; z-index: 2; line-height: normal }
|
text-align: left; z-index: 2; line-height: normal }
|
||||||
.dropdownMenu { cursor: pointer; font-size: 93%; position: relative }
|
.dropdownMenu { cursor: pointer; font-size: 93%; position: relative }
|
||||||
.dropdown a:hover { text-decoration: underline }
|
.dropdown a:hover { text-decoration: underline }
|
||||||
|
@ -133,7 +133,7 @@ td.search { padding-top: 8px }
|
||||||
.even { background-color: #fbfbfb }
|
.even { background-color: #fbfbfb }
|
||||||
.odd { background-color: #f6f6f6 }
|
.odd { background-color: #f6f6f6 }
|
||||||
.odd2 { background-color: #f2f2f2 }
|
.odd2 { background-color: #f2f2f2 }
|
||||||
.refMenuItem { border-top: grey 1px dashed; margin: 3px 0; padding-top: 3px }
|
.refMenuItem { border-top: grey 1px dashed; margin-top: 3px; padding-top: 3px }
|
||||||
.summary { margin-bottom: 5px; background-color: whitesmoke;
|
.summary { margin-bottom: 5px; background-color: whitesmoke;
|
||||||
border: 3px solid white }
|
border: 3px solid white }
|
||||||
.by { padding: 5px; color: grey; font-size: 97% }
|
.by { padding: 5px; color: grey; font-size: 97% }
|
||||||
|
|
|
@ -380,11 +380,12 @@ class ToolWrapper(AbstractWrapper):
|
||||||
</th>
|
</th>
|
||||||
<th for="column in columns"
|
<th for="column in columns"
|
||||||
var2="field=column.field;
|
var2="field=column.field;
|
||||||
sortable=ztool.isSortable(field.name, className, 'search');
|
sortable=field.isSortable(usage='search');
|
||||||
filterable=field.filterable"
|
filterable=field.filterable"
|
||||||
width=":column.width" align=":column.align">
|
width=":column.width" align=":column.align">
|
||||||
<x>::ztool.truncateText(_(field.labelId))</x>
|
<x>::ztool.truncateText(_(field.labelId))</x>
|
||||||
<x>:tool.pxSortAndFilter</x><x>:tool.pxShowDetails</x>
|
<x if="totalNumber > 1">:tool.pxSortAndFilter</x>
|
||||||
|
<x>:tool.pxShowDetails</x>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
|
@ -694,6 +694,8 @@ class AbstractWrapper(object):
|
||||||
n = field.name
|
n = field.name
|
||||||
indexName = 'get%s%s' % (n[0].upper(), n[1:])
|
indexName = 'get%s%s' % (n[0].upper(), n[1:])
|
||||||
res[indexName] = field.getIndexType()
|
res[indexName] = field.getIndexType()
|
||||||
|
# Add the secondary index if present
|
||||||
|
if field.hasSortIndex(): res['%s_sort' % indexName] = 'FieldIndex'
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
@ -1065,8 +1067,16 @@ class AbstractWrapper(object):
|
||||||
method in those cases.
|
method in those cases.
|
||||||
'''
|
'''
|
||||||
if fields:
|
if fields:
|
||||||
# Get names of indexes from field names.
|
# Get names of indexes from field names
|
||||||
indexes = [Search.getIndexName(name) for name in fields]
|
indexes = []
|
||||||
|
for name in fields:
|
||||||
|
field = self.getField(name)
|
||||||
|
if not field.indexed: continue
|
||||||
|
# A field may have 2 different indexes
|
||||||
|
iName = field.getIndexName(usage='search')
|
||||||
|
indexes.append(iName)
|
||||||
|
sName = field.getIndexName(usage='sort')
|
||||||
|
if sName != iName: indexes.append(sName)
|
||||||
else:
|
else:
|
||||||
indexes = None
|
indexes = None
|
||||||
self.o.reindex(indexes=indexes, unindex=unindex)
|
self.o.reindex(indexes=indexes, unindex=unindex)
|
||||||
|
|
Loading…
Reference in a new issue