[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:
Gaetan Delannay 2014-11-28 14:42:32 +01:00
parent 5c41f1b3d2
commit c11002b7d5
8 changed files with 100 additions and 66 deletions

View file

@ -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())

View file

@ -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) &gt; 1) and \ <x if="changeOrder and (len(objects) &gt; 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

View file

@ -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?'''

View file

@ -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

View file

@ -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)

View file

@ -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% }

View file

@ -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 &gt; 1">:tool.pxSortAndFilter</x>
<x>:tool.pxShowDetails</x>
</th> </th>
</tr> </tr>

View file

@ -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)