Improved advanced search functionality + completed String fields with Selection instance as validator.

This commit is contained in:
Gaetan Delannay 2010-01-06 18:36:16 +01:00
parent f8baeee4f7
commit d6607d7815
15 changed files with 333 additions and 84 deletions

View file

@ -21,15 +21,28 @@ class Page:
class Import:
'''Used for describing the place where to find the data to use for creating
an object.'''
def __init__(self, path, columnMethod=None, columnHeaders=(),
sortMethod=None):
def __init__(self, path, onElement=None, headers=(), sort=None):
self.id = 'import'
self.path = path
self.columnMethod = columnMethod
# This method allows to split every element into subElements that can
# be shown as column values in a table.
self.columnHeaders = columnHeaders
self.sortMethod = sortMethod
# p_onElement hereafter must be a function (or a static method) that
# will be called every time an element to import is found. It takes a
# single arg that is the absolute filen name of the file to import,
# within p_path. It must return a list of info about the element, or
# None if the element must be ignored. The list will be used to display
# information about the element in a tabular form.
self.onElement = onElement
# The following attribute must contain the names of the column headers
# of the table that will display elements to import (retrieved from
# calls to self.onElement). Every not-None element retrieved from
# self.onElement must have the same length as self.headers.
self.headers = headers
# The following attribute must store a function or static method that
# will be used to sort elements to import. It will be called with a
# single param containing the list of all not-None elements as retrieved
# by calls to self.onElement (but with one additional first element in
# every list, which is the absolute file name of the element to import)
# and must return a similar, sorted, list.
self.sort = sort
class Search:
'''Used for specifying a search for a given type.'''
@ -561,7 +574,25 @@ class Selection:
'''Instances of this class may be given as validator of a String, in order
to tell Appy that the validator is a selection that will be computed
dynamically.'''
pass
def __init__(self, methodName):
# The p_methodName parameter must be the name of a method that will be
# called every time Appy will need to get the list of possible values
# for the related field. It must correspond to an instance method of
# the class defining the related field. This method accepts no argument
# and must return a list (or tuple) of pairs (lists or tuples):
# (id, text), where "id" is one of the possible values for the field,
# and "text" is the value as will be shown on the screen. You can use
# self.translate within this method to produce an internationalized
# "text" if needed.
self.methodName = methodName
def getText(self, obj, value):
'''Gets the text that corresponds to p_value.'''
vocab = obj._appy_getDynamicDisplayList(self.methodName)
if type(value) in sequenceTypes:
return [vocab.getValue(v) for v in value]
else:
return vocab.getValue(value)
# ------------------------------------------------------------------------------
class Tool:

View file

@ -13,15 +13,20 @@ class Descriptor: # Abstract
class ClassDescriptor(Descriptor):
'''This class gives information about an Appy class.'''
def getOrderedAppyAttributes(self):
def getOrderedAppyAttributes(self, condition=None):
'''Returns the appy types for all attributes of this class and parent
class(es).'''
class(es). If a p_condition is specified, ony Appy types matching
the condition will be returned. p_condition must be a string
containing an expression that will be evaluated with, in its context,
"self" being this ClassDescriptor and "attrValue" being the current
Type instance.'''
res = []
# First, get the attributes for the current class
for attrName in self.orderedAttributes:
attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Type):
res.append( (attrName, attrValue) )
if not condition or eval(condition):
res.append( (attrName, attrValue) )
# Then, add attributes from parent classes
for baseClass in self.klass.__bases__:
# Find the classDescr that corresponds to baseClass

View file

@ -11,7 +11,7 @@ from utils import stringify
import appy.gen
import appy.gen.descriptors
from appy.gen.po import PoMessage
from appy.gen import Date, String, State, Transition, Type, Search
from appy.gen import Date, String, State, Transition, Type, Search, Selection
from appy.gen.utils import GroupDescr, PageDescr, produceNiceMessage, \
sequenceTypes
TABS = 4 # Number of blanks in a Python indentation.
@ -111,8 +111,7 @@ class ArchetypeFieldDescriptor:
# Elements common to all selection fields
methodName = 'list_%s_values' % self.fieldName
self.fieldParams['vocabulary'] = methodName
self.classDescr.addSelectMethod(
methodName, self, self.appyType.isMultiValued())
self.classDescr.addSelectMethod(methodName, self)
self.fieldParams['enforceVocabulary'] = True
else:
self.fieldType = 'StringField'
@ -370,7 +369,7 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
field = ArchetypeFieldDescriptor(attrName, attrValue, self)
self.schema += '\n' + field.generate()
def addSelectMethod(self, methodName, fieldDescr, isMultivalued=False):
def addSelectMethod(self, methodName, fieldDescr):
'''For the selection field p_fieldDescr I need to generate a method
named p_methodName that will generate the vocabulary for
p_fieldDescr.'''
@ -395,11 +394,16 @@ class ClassDescriptor(appy.gen.descriptors.ClassDescriptor):
# Generate a method that returns a DisplayList
appName = self.generator.applicationName
allValues = appyType.validator
if not isMultivalued:
if not appyType.isMultiValued():
allValues = [''] + appyType.validator
labels.insert(0, 'choose_a_value')
m += ' '*spaces + 'return self._appy_getDisplayList' \
'(%s, %s, %s)\n' % (s(allValues), s(labels), s(appName))
elif isinstance(appyType.validator, Selection):
# Call the custom method that will produce dynamically the list of
# values.
m += ' '*spaces + 'return self._appy_getDynamicDisplayList' \
'(%s)\n' % s(appyType.validator.methodName)
self.methods = m
def addValidateMethod(self, methodName, label, fieldDescr,

View file

@ -121,6 +121,7 @@ class Generator(AbstractGenerator):
msg('search_objects', '', msg.SEARCH_OBJECTS),
msg('search_results', '', msg.SEARCH_RESULTS),
msg('search_results_descr', '', ' '),
msg('search_new', '', msg.SEARCH_NEW),
msg('ref_invalid_index', '', msg.REF_INVALID_INDEX),
msg('bad_int', '', msg.BAD_INT),
msg('bad_float', '', msg.BAD_FLOAT),
@ -136,6 +137,7 @@ class Generator(AbstractGenerator):
msg('goto_next', '', msg.GOTO_NEXT),
msg('goto_last', '', msg.GOTO_LAST),
msg('goto_source', '', msg.GOTO_SOURCE),
msg('whatever', '', msg.WHATEVER),
]
# Create basic files (config.py, Install.py, etc)
self.generateTool()
@ -299,6 +301,12 @@ class Generator(AbstractGenerator):
theImport = 'import %s' % classDescr.klass.__module__
if theImport not in imports:
imports.append(theImport)
# Compute ordered lists of attributes for every Appy class.
attributes = []
for classDescr in classDescrs:
classAttrs = [a[0] for a in classDescr.getOrderedAppyAttributes()]
attrs = ','.join([('"%s"' % a) for a in classAttrs])
attributes.append('"%s":[%s]' % (classDescr.name, attrs))
# Compute root classes
rootClasses = ''
for classDescr in self.classes:
@ -317,6 +325,7 @@ class Generator(AbstractGenerator):
repls['referers'] = referers
repls['workflowInstancesInit'] = wfInit
repls['imports'] = '\n'.join(imports)
repls['attributes'] = ',\n '.join(attributes)
repls['defaultAddRoles'] = ','.join(
['"%s"' % r for r in self.config.defaultCreators])
repls['addPermissions'] = addPermissions
@ -598,9 +607,10 @@ class Generator(AbstractGenerator):
fieldType.group = childDescr.klass.__name__
Flavour._appy_addField(childFieldName,fieldType,childDescr)
if classDescr.isRoot():
# We must be able to configure query results from the
# flavour.
# We must be able to configure query results from the flavour.
Flavour._appy_addQueryResultColumns(classDescr)
# Add the search-related fields.
Flavour._appy_addSearchRelatedFields(classDescr)
Flavour._appy_addWorkflowFields(self.flavourDescr)
Flavour._appy_addWorkflowFields(self.podTemplateDescr)
# Generate the flavour class and related i18n messages

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
# ------------------------------------------------------------------------------
import copy, types
from appy.gen import Type, Integer, String, File, Ref, Boolean
from appy.gen import Type, Integer, String, File, Ref, Boolean, Selection
# ------------------------------------------------------------------------------
class ModelClass:
@ -22,13 +22,14 @@ class ModelClass:
# instance of a ModelClass, those attributes must not be
# given in the constructor.
@classmethod
def _appy_addField(klass, fieldName, fieldType, classDescr):
exec "klass.%s = fieldType" % fieldName
klass._appy_attributes.append(fieldName)
if hasattr(klass, '_appy_classes'):
klass._appy_classes[fieldName] = classDescr.name
_appy_addField = classmethod(_appy_addField)
@classmethod
def _appy_getTypeBody(klass, appyType):
'''This method returns the code declaration for p_appyType.'''
typeArgs = ''
@ -45,10 +46,12 @@ class ModelClass:
attrValue = attrValue.__name__
else:
attrValue = '%s.%s' % (moduleName, attrValue.__name__)
elif isinstance(attrValue, Selection):
attrValue = 'Selection("%s")' % attrValue.methodName
typeArgs += '%s=%s,' % (attrName, attrValue)
return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
_appy_getTypeBody = classmethod(_appy_getTypeBody)
@classmethod
def _appy_getBody(klass):
'''This method returns the code declaration of this class. We will dump
this in appyWrappers.py in the resulting product.'''
@ -57,7 +60,6 @@ class ModelClass:
exec 'appyType = klass.%s' % attrName
res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType))
return res
_appy_getBody = classmethod(_appy_getBody)
class PodTemplate(ModelClass):
description = String(format=String.TEXT)
@ -87,6 +89,7 @@ class Flavour(ModelClass):
# We need to remember the original classes related to the flavour attributes
_appy_attributes = list(defaultFlavourAttrs)
@classmethod
def _appy_clean(klass):
toClean = []
for k, v in klass.__dict__.iteritems():
@ -97,8 +100,8 @@ class Flavour(ModelClass):
exec 'del klass.%s' % k
klass._appy_attributes = list(defaultFlavourAttrs)
klass._appy_classes = {}
_appy_clean = classmethod(_appy_clean)
@classmethod
def _appy_copyField(klass, appyType):
'''From a given p_appyType, produce a type definition suitable for
storing the default value for this field.'''
@ -121,8 +124,8 @@ class Flavour(ModelClass):
res.back.show = False
res.select = None # Not callable from flavour
return res
_appy_copyField = classmethod(_appy_copyField)
@classmethod
def _appy_addOptionalField(klass, fieldDescr):
className = fieldDescr.classDescr.name
fieldName = 'optionalFieldsFor%s' % className
@ -134,8 +137,8 @@ class Flavour(ModelClass):
fieldType.validator.append(fieldDescr.fieldName)
fieldType.page = 'data'
fieldType.group = fieldDescr.classDescr.klass.__name__
_appy_addOptionalField = classmethod(_appy_addOptionalField)
@classmethod
def _appy_addDefaultField(klass, fieldDescr):
className = fieldDescr.classDescr.name
fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName)
@ -143,8 +146,8 @@ class Flavour(ModelClass):
klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr)
fieldType.page = 'data'
fieldType.group = fieldDescr.classDescr.klass.__name__
_appy_addDefaultField = classmethod(_appy_addDefaultField)
@classmethod
def _appy_addPodField(klass, classDescr):
'''Adds a POD field to the flavour and also an integer field that will
determine the maximum number of documents to show at once on consult
@ -163,21 +166,47 @@ class Flavour(ModelClass):
klass._appy_addField(fieldName, fieldType, classDescr)
classDescr.flavourFieldsToPropagate.append(
('podMaxShownTemplatesFor%s', copy.copy(fieldType)) )
_appy_addPodField = classmethod(_appy_addPodField)
@classmethod
def _appy_addQueryResultColumns(klass, classDescr):
'''Adds, for class p_classDescr, the attribute in the flavour that
allows to select what default columns will be shown on query
results.'''
className = classDescr.name
fieldName = 'resultColumnsFor%s' % className
attrNames = [a[0] for a in classDescr.getOrderedAppyAttributes()]
attrNames.append('workflowState') # Object state from workflow
if 'title' in attrNames:
attrNames.remove('title') # Included by default.
fieldType = String(multiplicity=(0,None), validator=attrNames,
page='userInterface',
group=classDescr.klass.__name__)
fieldType = String(multiplicity=(0,None), validator=Selection(
'_appy_getAllFields*%s' % className), page='userInterface',
group=classDescr.klass.__name__)
klass._appy_addField(fieldName, fieldType, classDescr)
_appy_addQueryResultColumns = classmethod(_appy_addQueryResultColumns)
@classmethod
def _appy_addSearchRelatedFields(klass, classDescr):
'''Adds, for class p_classDescr, attributes related to the search
functionality for class p_classDescr.'''
className = classDescr.name
# Field that defines if advanced search is enabled for class
# p_classDescr or not.
fieldName = 'enableAdvancedSearchFor%s' % className
fieldType = Boolean(default=True, page='userInterface',
group=classDescr.klass.__name__)
klass._appy_addField(fieldName, fieldType, classDescr)
# Field that defines how many columns are shown on the custom search
# screen.
fieldName = 'numberOfSearchColumnsFor%s' % className
fieldType = Integer(default=3, page='userInterface',
group=classDescr.klass.__name__)
klass._appy_addField(fieldName, fieldType, classDescr)
# Field that allows to select, among all indexed fields, what fields
# must really be used in the search screen.
fieldName = 'searchFieldsFor%s' % className
defaultValue = [a[0] for a in classDescr.getOrderedAppyAttributes(
condition='attrValue.indexed')]
fieldType = String(multiplicity=(0,None), validator=Selection(
'_appy_getSearchableFields*%s' % className), default=defaultValue,
page='userInterface', group=classDescr.klass.__name__)
klass._appy_addField(fieldName, fieldType, classDescr)
@classmethod
def _appy_addWorkflowFields(klass, classDescr):
'''Adds, for a given p_classDescr, the workflow-related fields.'''
className = classDescr.name
@ -209,8 +238,6 @@ class Flavour(ModelClass):
group=groupName)
klass._appy_addField(fieldName, fieldType, classDescr)
_appy_addWorkflowFields = classmethod(_appy_addWorkflowFields)
class Tool(ModelClass):
flavours = Ref(None, multiplicity=(1,None), add=True, link=False,
back=Ref(attribute='tool'))

View file

@ -647,7 +647,8 @@
totalNumber queryResult/totalNumber;
batchSize queryResult/batchSize;
ajaxHookId python:'queryResult';
baseUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName, startNumber='**v**')">
baseUrl python: tool.getQueryUrl(contentType, flavourNumber, searchName, startNumber='**v**');
newSearchUrl python: '%s/skyn/search?type_name=%s&flavourNumber=%d' % (tool.getAppFolder().absolute_url(), contentType, flavourNumber);">
<tal:result condition="objs">
@ -655,6 +656,10 @@
<legend>
<span tal:replace="structure python: test(searchName, tool.translate(searchLabel), test(severalTypes, tool.translate(tool.getAppName()), tool.translate('%s_plural' % contentType)))"/>
(<span tal:replace="totalNumber"/>)
<tal:newSearch condition="python: searchName == '_advanced'">
&nbsp;&nbsp;—&nbsp;&nbsp;<i><a tal:attributes="href newSearchUrl"
tal:content="python: tool.translate('search_new')"></a></i>
</tal:newSearch>
</legend>
<table cellpadding="0" cellspacing="0" width="100%"><tr>
@ -787,9 +792,13 @@
</fieldset>
</tal:result>
<span tal:condition="not: objs"
tal:content="python: tool.translate('query_no_result')">No result.
</span>
<tal:noResult condition="not: objs">
<span tal:replace="python: tool.translate('query_no_result')"/>
<tal:newSearch condition="python: searchName == '_advanced'">
<br/><i class="discreet"><a tal:attributes="href newSearchUrl"
tal:content="python: tool.translate('search_new')"></a></i>
</tal:newSearch>
</tal:noResult>
</metal:queryResults>
@ -883,7 +892,9 @@
<tal:comment replace="nothing">TODO: implement a widget for selecting the needed flavour.</tal:comment>
<tal:comment replace="nothing">Create a section for every root class.</tal:comment>
<tal:section repeat="rootClass rootClasses" define="flavourNumber python:1">
<tal:section repeat="rootClass rootClasses"
define="flavourNumber python:1;
flavour python: tool.getFlavour('Dummy_%d' % flavourNumber)">
<tal:comment replace="nothing">Section title, with action icons</tal:comment>
<dt tal:attributes="class python:test(repeat['rootClass'].number()==1, 'portletAppyItem', 'portletAppyItem portletSep')">
<table width="100%" cellspacing="0" cellpadding="0" class="no-style-table">
@ -911,6 +922,8 @@
title python: tool.translate('query_import')"/>
<tal:comment replace="nothing">Search objects of this type (todo: update flavourNumber)</tal:comment>
<img style="cursor:pointer"
tal:define="showSearch python: flavour.getAttr('enableAdvancedSearchFor%s' % rootClass)"
tal:condition="showSearch"
tal:attributes="onClick python: 'href: window.location=\'%s/skyn/search?type_name=%s&flavourNumber=1\'' % (appFolder.absolute_url(), rootClass);
src string: $portal_url/skyn/search.gif;
title python: tool.translate('search_objects')"/>

View file

@ -19,7 +19,7 @@
<table class="no-style-table" cellpadding="0" cellspacing="0">
<tr>
<tal:comment replace="nothing">Arrows for moving objects up or down</tal:comment>
<td class="noPadding" tal:condition="python: (len(objs)&gt;1) and member.has_permission('Modify portal content', obj)">
<td class="noPadding" tal:condition="python: (len(objs)&gt;1) and member.has_permission('Modify portal content', contextObj)">
<tal:moveRef define="objectIndex python:contextObj.getAppyRefIndex(fieldName, obj);
baseUrl python: contextObj.getUrl('showRef', **{'fieldName': fieldName, 'isBack': isBack, 'innerRef': innerRef, 'labelId': labelId, 'descrId': descrId, '%s_startNumber' % ajaxHookId: startNumber, 'action':'ChangeRefOrder', 'refObjectUid': obj.UID()})">
<tal:comment replace="nothing">Move up</tal:comment>

View file

@ -16,10 +16,12 @@
tal:define="appFolder context/getParentNode;
contentType request/type_name;
tool python: portal.get('portal_%s' % appFolder.id.lower());
searchableFields python: tool.getSearchableFields(contentType)">
flavour python: tool.getFlavour('Dummy_%s' % request['flavourNumber']);
searchableFields python: flavour.getSearchableFields(contentType)">
<tal:comment replace="nothing">Search title</tal:comment>
<h1 tal:content="python: tool.translate('search_title')"></h1><br/>
<h1><span tal:replace="python: tool.translate('%s_plural' % contentType)"/> —
<span tal:replace="python: tool.translate('search_title')"/></h1><br/>
<tal:comment replace="nothing">Form for searching objects of request/type_name.</tal:comment>
<form name="search" tal:attributes="action python: appFolder.absolute_url()+'/skyn/do'" method="post">
@ -27,12 +29,20 @@
<input type="hidden" name="type_name" tal:attributes="value contentType"/>
<input type="hidden" name="flavourNumber:int" tal:attributes="value request/flavourNumber"/>
<tal:searchFields repeat="searchField searchableFields">
<tal:showSearchField define="fieldName python:searchField[0];
appyType python:searchField[1]">
<metal:searchField use-macro="python: appFolder.skyn.widgets.macros.get('search%s' % searchField[1]['type'])"/>
</tal:showSearchField>
</tal:searchFields>
<table class="no-style-table" cellpadding="0" cellspacing="0" width="100%"
tal:define="numberOfColumns python: flavour.getAttr('numberOfSearchColumnsFor%s' % contentType)">
<tr tal:repeat="searchRow python: tool.tabularize(searchableFields, numberOfColumns)" valign="top" class="appySearchRow">
<td tal:repeat="searchField searchRow">
<tal:field condition="searchField">
<tal:showSearchField define="fieldName python:searchField[0];
appyType python:searchField[1];
widgetName python: 'w_%s' % fieldName">
<metal:searchField use-macro="python: appFolder.skyn.widgets.macros.get('search%s' % searchField[1]['type'])"/>
</tal:showSearchField>
</tal:field><br/><br class="discreet"/>
</td>
</tr>
</table>
<tal:comment replace="nothing">Submit button</tal:comment>
<p align="right"><br/>

View file

@ -7,11 +7,35 @@
</metal:searchFloat>
<metal:searchString define-macro="searchString">
<p tal:content="fieldName">Hello</p>
<label tal:attributes="for widgetName" tal:content="python: tool.translate(appyType['label'])"></label><br>&nbsp;&nbsp;
<tal:comment replace="nothing">Show a simple search field for most String fields.</tal:comment>
<tal:simpleSearch condition="not: appyType/isSelect">
<input type="text" tal:attributes="name widgetName"/>
</tal:simpleSearch>
<tal:comment replace="nothing">Show a multi-selection box for fields whose validator defines a list of values.</tal:comment>
<tal:selectSearch condition="appyType/isSelect">
<select tal:attributes="name widgetName" multiple="multiple" size="5">
<option tal:repeat="v appyType/validator" tal:attributes="value v"
tal:content="python: tool.truncate(tool.translate('%s_list_%s' % (appyType['label'], v)), 70)">
</option>
</select>
</tal:selectSearch>
</metal:searchString>
<metal:searchBoolean define-macro="searchBoolean">
<p tal:content="fieldName">Hello</p>
<metal:searchBoolean define-macro="searchBoolean" tal:define="typedWidget python:'%s*bool' % widgetName">
<label tal:attributes="for widgetName" tal:content="python: tool.translate(appyType['label'])"></label><br>&nbsp;&nbsp;
<tal:yes define="valueId python:'%s_yes' % fieldName">
<input type="radio" class="noborder" value="True" tal:attributes="name typedWidget; id valueId"/>
<label tal:attributes="for valueId" i18n:translate="yes" i18n:domain="plone"></label>
</tal:yes>
<tal:no define="valueId python:'%s_no' % fieldName">
<input type="radio" class="noborder" value="False" tal:attributes="name typedWidget; id valueId"/>
<label tal:attributes="for valueId" i18n:translate="no" i18n:domain="plone"></label>
</tal:no>
<tal:whatever define="valueId python:'%s_whatever' % fieldName">
<input type="radio" class="noborder" value="" tal:attributes="name typedWidget; id valueId" checked="checked"/>
<label tal:attributes="for valueId" tal:content="python: tool.translate('whatever')"></label>
</tal:whatever>
</metal:searchBoolean>
<metal:searchDate define-macro="searchDate">

View file

@ -42,4 +42,8 @@ referers = {
# names of DC transitions.
workflowInstances = {}
<!workflowInstancesInit!>
# In the following dict, we store, for every Appy class, the ordered list of
# attributes (included inherited attributes).
attributes = {<!attributes!>}
# ------------------------------------------------------------------------------

View file

@ -41,6 +41,18 @@ class FlavourWrapper:
Stores the list of columns that must be show when displaying
instances of the a given root p_klass.
"enableAdvancedSearch"
Determines if the advanced search screen must be enabled for
p_klass.
"numberOfSearchColumns"
Determines in how many columns the search screen for p_klass
is rendered.
"searchFields"
Determines, among all indexed fields for p_klass, which one will
really be used in the search screen.
"optionalFields"
Stores the list of optional attributes that are in use in the
current flavour for the given p_klass.

View file

@ -61,6 +61,7 @@ class PoMessage:
SEARCH_BUTTON = 'Search'
SEARCH_OBJECTS = 'Search objects of this type.'
SEARCH_RESULTS = 'Search results'
SEARCH_NEW = 'New search'
WORKFLOW_COMMENT = 'Optional comment'
WORKFLOW_STATE = 'state'
DATA_CHANGE = 'Data change'
@ -93,6 +94,7 @@ class PoMessage:
GOTO_NEXT = 'Go to next'
GOTO_LAST = 'Go to end'
GOTO_SOURCE = 'Go back'
WHATEVER = 'Whatever'
def __init__(self, id, msg, default, fuzzy=False, comments=[]):
self.id = id