Improved advanced search functionality + completed String fields with Selection instance as validator.
This commit is contained in:
parent
f8baeee4f7
commit
d6607d7815
15 changed files with 333 additions and 84 deletions
|
@ -1,5 +1,6 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import appy.gen
|
||||
from appy.gen import Type
|
||||
from appy.gen.plone25.mixins import AbstractMixin
|
||||
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
|
||||
|
||||
|
@ -110,4 +111,41 @@ class FlavourMixin(AbstractMixin):
|
|||
'''Gets on this flavour attribute named p_attrName. Useful because we
|
||||
can't use getattr directly in Zope Page Templates.'''
|
||||
return getattr(self, attrName, None)
|
||||
|
||||
def _appy_getAllFields(self, contentType):
|
||||
'''Returns the (translated) names of fields of p_contentType.'''
|
||||
res = []
|
||||
for attrName in self.getProductConfig().attributes[contentType]:
|
||||
if attrName != 'title': # Will be included by default.
|
||||
label = '%s_%s' % (contentType, attrName)
|
||||
res.append((attrName, self.translate(label)))
|
||||
# Add object state
|
||||
res.append(('workflowState', self.translate('workflow_state')))
|
||||
return res
|
||||
|
||||
def _appy_getSearchableFields(self, contentType):
|
||||
'''Returns the (translated) names of fields that may be searched on
|
||||
objects of type p_contentType (=indexed fields).'''
|
||||
tool = self.getParentNode()
|
||||
appyClass = tool.getAppyClass(contentType)
|
||||
attrNames = self.getProductConfig().attributes[contentType]
|
||||
res = []
|
||||
for attrName in attrNames:
|
||||
attr = getattr(appyClass, attrName)
|
||||
if isinstance(attr, Type) and attr.indexed:
|
||||
label = '%s_%s' % (contentType, attrName)
|
||||
res.append((attrName, self.translate(label)))
|
||||
return res
|
||||
|
||||
def getSearchableFields(self, contentType):
|
||||
'''Returns, among the list of all searchable fields (see method above),
|
||||
the list of fields that the user has configured in the flavour as
|
||||
being effectively used in the search screen.'''
|
||||
res = []
|
||||
appyClass = self.getAppyClass(contentType)
|
||||
for attrName in getattr(self, 'searchFieldsFor%s' % contentType):
|
||||
attr = getattr(appyClass, attrName)
|
||||
dAttr = self._appy_getTypeAsDict(attrName, attr, appyClass)
|
||||
res.append((attrName, dAttr))
|
||||
return res
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import re, os, os.path, Cookie
|
||||
from appy.gen import Type
|
||||
from appy.gen import Type, Search
|
||||
from appy.gen.utils import FieldDescr, SomeObjects
|
||||
from appy.gen.plone25.mixins import AbstractMixin
|
||||
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
|
||||
|
@ -117,7 +117,7 @@ class ToolMixin(AbstractMixin):
|
|||
will be added to the query, or;
|
||||
2) "_advanced": in this case, additional search criteria will also
|
||||
be added to the query, but those criteria come from the session
|
||||
and were created from search.pt.
|
||||
(in key "searchCriteria") and were created from search.pt.
|
||||
|
||||
We will retrieve objects from p_startNumber. If p_search is defined,
|
||||
it corresponds to a custom Search instance (instead of a predefined
|
||||
|
@ -154,6 +154,9 @@ class ToolMixin(AbstractMixin):
|
|||
if searchName != '_advanced':
|
||||
search = ArchetypesClassDescriptor.getSearch(
|
||||
appyClass, searchName)
|
||||
else:
|
||||
fields = self.REQUEST.SESSION['searchCriteria']
|
||||
search = Search('customSearch', **fields)
|
||||
if search:
|
||||
# Add additional search criteria
|
||||
for fieldName, fieldValue in search.fields.iteritems():
|
||||
|
@ -162,7 +165,12 @@ class ToolMixin(AbstractMixin):
|
|||
elif attrName == 'description': attrName = 'Description'
|
||||
elif attrName == 'state': attrName = 'review_state'
|
||||
else: attrName = 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
|
||||
params[attrName] = fieldValue
|
||||
if isinstance(fieldValue, basestring) and \
|
||||
fieldValue.endswith('*'):
|
||||
v = fieldValue[:-1]
|
||||
params[attrName] = {'query':[v,v+'Z'], 'range':'minmax'}
|
||||
else:
|
||||
params[attrName] = fieldValue
|
||||
# Add a sort order if specified
|
||||
sb = search.sortBy
|
||||
if sb:
|
||||
|
@ -335,19 +343,20 @@ class ToolMixin(AbstractMixin):
|
|||
p_contentType.'''
|
||||
appyClass = self.getAppyClass(contentType)
|
||||
importParams = self.getCreateMeans(appyClass)['import']
|
||||
columnMethod = importParams['columnMethod'].__get__('')
|
||||
sortMethod = importParams['sortMethod']
|
||||
onElement = importParams['onElement'].__get__('')
|
||||
sortMethod = importParams['sort']
|
||||
if sortMethod: sortMethod = sortMethod.__get__('')
|
||||
elems = []
|
||||
for elem in os.listdir(importParams['path']):
|
||||
elemFullPath = os.path.join(importParams['path'], elem)
|
||||
niceElem = columnMethod(elemFullPath)
|
||||
niceElem.insert(0, elemFullPath) # To the result, I add the full
|
||||
# path of the elem, which will not be shown.
|
||||
elems.append(niceElem)
|
||||
elemInfo = onElement(elemFullPath)
|
||||
if elemInfo:
|
||||
elemInfo.insert(0, elemFullPath) # To the result, I add the full
|
||||
# path of the elem, which will not be shown.
|
||||
elems.append(elemInfo)
|
||||
if sortMethod:
|
||||
elems = sortMethod(elems)
|
||||
return [importParams['columnHeaders'], elems]
|
||||
return [importParams['headers'], elems]
|
||||
|
||||
def onImportObjects(self):
|
||||
'''This method is called when the user wants to create objects from
|
||||
|
@ -371,22 +380,25 @@ class ToolMixin(AbstractMixin):
|
|||
else:
|
||||
return False
|
||||
|
||||
def getSearchableFields(self, contentType):
|
||||
'''Returns the list of fields that may be searched on objects on type
|
||||
p_contentType (=indexed fields).'''
|
||||
appyClass = self.getAppyClass(contentType)
|
||||
res = []
|
||||
for attrName in dir(appyClass):
|
||||
attr = getattr(appyClass, attrName)
|
||||
if isinstance(attr, Type) and attr.indexed:
|
||||
dAttr = self._appy_getTypeAsDict(attrName, attr, appyClass)
|
||||
res.append((attrName, dAttr))
|
||||
return res
|
||||
|
||||
def onSearchObjects(self):
|
||||
'''This method is called when the user triggers a search from
|
||||
search.pt.'''
|
||||
rq = self.REQUEST
|
||||
# Store the search criteria in the session
|
||||
criteria = {}
|
||||
for attrName in rq.form.keys():
|
||||
if attrName.startswith('w_'):
|
||||
attrValue = rq.form[attrName]
|
||||
if attrValue:
|
||||
if attrName.find('*') != -1:
|
||||
attrName, attrType = attrName.split('*')
|
||||
if attrType == 'bool':
|
||||
exec 'attrValue = %s' % attrValue
|
||||
if isinstance(attrValue, list):
|
||||
attrValue = ' OR '.join(attrValue)
|
||||
criteria[attrName[2:]] = attrValue
|
||||
rq.SESSION['searchCriteria'] = criteria
|
||||
# Goto the screen that displays search results
|
||||
backUrl = '%s/query?type_name=%s&flavourNumber=%d&search=_advanced' % \
|
||||
(os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber'])
|
||||
return self.goto(backUrl)
|
||||
|
@ -556,4 +568,30 @@ class ToolMixin(AbstractMixin):
|
|||
navUrl = baseUrl + '/?nav=' + newNav % (index + 1)
|
||||
res['%sUrl' % urlType] = navUrl
|
||||
return res
|
||||
|
||||
def tabularize(self, data, numberOfRows):
|
||||
'''This method transforms p_data, which must be a "flat" list or tuple,
|
||||
into a list of lists, where every sub-list has length p_numberOfRows.
|
||||
This method is typically used for rendering elements in a table of
|
||||
p_numberOfRows rows.'''
|
||||
if numberOfRows > 1:
|
||||
res = []
|
||||
row = []
|
||||
for elem in data:
|
||||
row.append(elem)
|
||||
if len(row) == numberOfRows:
|
||||
res.append(row)
|
||||
row = []
|
||||
# Complete the last unfinished line if required.
|
||||
if row:
|
||||
while len(row) < numberOfRows: row.append(None)
|
||||
res.append(row)
|
||||
return res
|
||||
else:
|
||||
return data
|
||||
|
||||
def truncate(self, value, numberOfChars):
|
||||
'''Truncates string p_value to p_numberOfChars.'''
|
||||
if len(value) > numberOfChars: return value[:numberOfChars] + '...'
|
||||
return value
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
import os, os.path, sys, types, mimetypes
|
||||
import appy.gen
|
||||
from appy.gen import String
|
||||
from appy.gen import String, Selection
|
||||
from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \
|
||||
ValidationErrors, sequenceTypes, SomeObjects
|
||||
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
|
||||
|
@ -216,13 +216,20 @@ class AbstractMixin:
|
|||
elif vType == 'String':
|
||||
if not v: return v
|
||||
if appyType['isSelect']:
|
||||
maxMult = appyType['multiplicity'][1]
|
||||
t = self.translate
|
||||
if (maxMult == None) or (maxMult > 1):
|
||||
return [t('%s_%s_list_%s' % (self.meta_type, name, e)) \
|
||||
for e in v]
|
||||
validator = appyType['validator']
|
||||
if isinstance(validator, Selection):
|
||||
# Value(s) come from a dynamic vocabulary
|
||||
return validator.getText(self, v)
|
||||
else:
|
||||
return t('%s_%s_list_%s' % (self.meta_type, name, v))
|
||||
# Value(s) come from a fixed vocabulary whose texts are in
|
||||
# i18n files.
|
||||
maxMult = appyType['multiplicity'][1]
|
||||
t = self.translate
|
||||
if (maxMult == None) or (maxMult > 1):
|
||||
return [t('%s_%s_list_%s' % (self.meta_type, name, e)) \
|
||||
for e in v]
|
||||
else:
|
||||
return t('%s_%s_list_%s' % (self.meta_type, name, v))
|
||||
return v
|
||||
elif vType == 'Boolean':
|
||||
if v: return self.translate('yes', domain='plone')
|
||||
|
@ -866,6 +873,10 @@ class AbstractMixin:
|
|||
# I create a new entry "backd"; if I put the dict in "back" I
|
||||
# really modify the initial appyType object and I don't want to do
|
||||
# this.
|
||||
# Add the i18n label for the field
|
||||
if not res.has_key('label'):
|
||||
res['label'] = '%s_%s' % (self._appy_getAtType(appyType.selfClass),
|
||||
fieldName)
|
||||
return res
|
||||
|
||||
def _appy_getAtType(self, appyClass, flavour=None):
|
||||
|
@ -979,6 +990,26 @@ class AbstractMixin:
|
|||
res.append( (v, self.utranslate(labels[i], domain=domain)))
|
||||
return self.getProductConfig().DisplayList(tuple(res))
|
||||
|
||||
def _appy_getDynamicDisplayList(self, methodName):
|
||||
'''Calls the method named p_methodName for producing a DisplayList from
|
||||
values computed dynamically. If methodName begins with _appy_, it is
|
||||
a special Appy method: we will call it on the Mixin directly. Else,
|
||||
it is a user method: we will call it on the wrapper. Some args can
|
||||
be hidden into p_methodName, separated with stars, like in this
|
||||
example: method1*arg1*arg2. Only string params are supported.'''
|
||||
# Unwrap parameters if any.
|
||||
if methodName.find('*') != -1:
|
||||
elems = methodName.split('*')
|
||||
methodName = elems[0]
|
||||
args = elems[1:]
|
||||
else:
|
||||
args = ()
|
||||
if methodName.startswith('_appy_'):
|
||||
exec 'res = self.%s(*args)' % methodName
|
||||
else:
|
||||
exec 'res = self.appy().%s(*args)' % methodName
|
||||
return self.getProductConfig().DisplayList(tuple(res))
|
||||
|
||||
nullValues = (None, '', ' ')
|
||||
numbersMap = {'Integer': 'int', 'Float': 'float'}
|
||||
validatorTypes = (types.FunctionType, type(String.EMAIL))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue