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: class Import:
'''Used for describing the place where to find the data to use for creating '''Used for describing the place where to find the data to use for creating
an object.''' an object.'''
def __init__(self, path, columnMethod=None, columnHeaders=(), def __init__(self, path, onElement=None, headers=(), sort=None):
sortMethod=None):
self.id = 'import' self.id = 'import'
self.path = path self.path = path
self.columnMethod = columnMethod # p_onElement hereafter must be a function (or a static method) that
# This method allows to split every element into subElements that can # will be called every time an element to import is found. It takes a
# be shown as column values in a table. # single arg that is the absolute filen name of the file to import,
self.columnHeaders = columnHeaders # within p_path. It must return a list of info about the element, or
self.sortMethod = sortMethod # 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: class Search:
'''Used for specifying a search for a given type.''' '''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 '''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 to tell Appy that the validator is a selection that will be computed
dynamically.''' 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: class Tool:

View file

@ -13,14 +13,19 @@ class Descriptor: # Abstract
class ClassDescriptor(Descriptor): class ClassDescriptor(Descriptor):
'''This class gives information about an Appy class.''' '''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 '''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 = [] res = []
# First, get the attributes for the current class # First, get the attributes for the current class
for attrName in self.orderedAttributes: for attrName in self.orderedAttributes:
attrValue = getattr(self.klass, attrName) attrValue = getattr(self.klass, attrName)
if isinstance(attrValue, Type): if isinstance(attrValue, Type):
if not condition or eval(condition):
res.append( (attrName, attrValue) ) res.append( (attrName, attrValue) )
# Then, add attributes from parent classes # Then, add attributes from parent classes
for baseClass in self.klass.__bases__: for baseClass in self.klass.__bases__:

View file

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

View file

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

View file

@ -1,5 +1,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import appy.gen import appy.gen
from appy.gen import Type
from appy.gen.plone25.mixins import AbstractMixin from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor 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 '''Gets on this flavour attribute named p_attrName. Useful because we
can't use getattr directly in Zope Page Templates.''' can't use getattr directly in Zope Page Templates.'''
return getattr(self, attrName, None) 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 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.utils import FieldDescr, SomeObjects
from appy.gen.plone25.mixins import AbstractMixin from appy.gen.plone25.mixins import AbstractMixin
from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin from appy.gen.plone25.mixins.FlavourMixin import FlavourMixin
@ -117,7 +117,7 @@ class ToolMixin(AbstractMixin):
will be added to the query, or; will be added to the query, or;
2) "_advanced": in this case, additional search criteria will also 2) "_advanced": in this case, additional search criteria will also
be added to the query, but those criteria come from the session 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, We will retrieve objects from p_startNumber. If p_search is defined,
it corresponds to a custom Search instance (instead of a predefined it corresponds to a custom Search instance (instead of a predefined
@ -154,6 +154,9 @@ class ToolMixin(AbstractMixin):
if searchName != '_advanced': if searchName != '_advanced':
search = ArchetypesClassDescriptor.getSearch( search = ArchetypesClassDescriptor.getSearch(
appyClass, searchName) appyClass, searchName)
else:
fields = self.REQUEST.SESSION['searchCriteria']
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():
@ -162,6 +165,11 @@ class ToolMixin(AbstractMixin):
elif attrName == 'description': attrName = 'Description' elif attrName == 'description': attrName = 'Description'
elif attrName == 'state': attrName = 'review_state' elif attrName == 'state': attrName = 'review_state'
else: attrName = 'get%s%s'% (fieldName[0].upper(),fieldName[1:]) else: attrName = 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
if isinstance(fieldValue, basestring) and \
fieldValue.endswith('*'):
v = fieldValue[:-1]
params[attrName] = {'query':[v,v+'Z'], 'range':'minmax'}
else:
params[attrName] = fieldValue params[attrName] = fieldValue
# Add a sort order if specified # Add a sort order if specified
sb = search.sortBy sb = search.sortBy
@ -335,19 +343,20 @@ class ToolMixin(AbstractMixin):
p_contentType.''' p_contentType.'''
appyClass = self.getAppyClass(contentType) appyClass = self.getAppyClass(contentType)
importParams = self.getCreateMeans(appyClass)['import'] importParams = self.getCreateMeans(appyClass)['import']
columnMethod = importParams['columnMethod'].__get__('') onElement = importParams['onElement'].__get__('')
sortMethod = importParams['sortMethod'] sortMethod = importParams['sort']
if sortMethod: sortMethod = sortMethod.__get__('') if sortMethod: sortMethod = sortMethod.__get__('')
elems = [] elems = []
for elem in os.listdir(importParams['path']): for elem in os.listdir(importParams['path']):
elemFullPath = os.path.join(importParams['path'], elem) elemFullPath = os.path.join(importParams['path'], elem)
niceElem = columnMethod(elemFullPath) elemInfo = onElement(elemFullPath)
niceElem.insert(0, elemFullPath) # To the result, I add the full if elemInfo:
elemInfo.insert(0, elemFullPath) # To the result, I add the full
# path of the elem, which will not be shown. # path of the elem, which will not be shown.
elems.append(niceElem) elems.append(elemInfo)
if sortMethod: if sortMethod:
elems = sortMethod(elems) elems = sortMethod(elems)
return [importParams['columnHeaders'], elems] return [importParams['headers'], elems]
def onImportObjects(self): def onImportObjects(self):
'''This method is called when the user wants to create objects from '''This method is called when the user wants to create objects from
@ -371,22 +380,25 @@ class ToolMixin(AbstractMixin):
else: else:
return False 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): def onSearchObjects(self):
'''This method is called when the user triggers a search from '''This method is called when the user triggers a search from
search.pt.''' search.pt.'''
rq = self.REQUEST 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' % \ backUrl = '%s/query?type_name=%s&flavourNumber=%d&search=_advanced' % \
(os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber']) (os.path.dirname(rq['URL']), rq['type_name'], rq['flavourNumber'])
return self.goto(backUrl) return self.goto(backUrl)
@ -556,4 +568,30 @@ class ToolMixin(AbstractMixin):
navUrl = baseUrl + '/?nav=' + newNav % (index + 1) navUrl = baseUrl + '/?nav=' + newNav % (index + 1)
res['%sUrl' % urlType] = navUrl res['%sUrl' % urlType] = navUrl
return res 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 os, os.path, sys, types, mimetypes
import appy.gen import appy.gen
from appy.gen import String from appy.gen import String, Selection
from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \ from appy.gen.utils import FieldDescr, GroupDescr, PhaseDescr, StateDescr, \
ValidationErrors, sequenceTypes, SomeObjects ValidationErrors, sequenceTypes, SomeObjects
from appy.gen.plone25.descriptors import ArchetypesClassDescriptor from appy.gen.plone25.descriptors import ArchetypesClassDescriptor
@ -216,6 +216,13 @@ class AbstractMixin:
elif vType == 'String': elif vType == 'String':
if not v: return v if not v: return v
if appyType['isSelect']: if appyType['isSelect']:
validator = appyType['validator']
if isinstance(validator, Selection):
# Value(s) come from a dynamic vocabulary
return validator.getText(self, v)
else:
# Value(s) come from a fixed vocabulary whose texts are in
# i18n files.
maxMult = appyType['multiplicity'][1] maxMult = appyType['multiplicity'][1]
t = self.translate t = self.translate
if (maxMult == None) or (maxMult > 1): if (maxMult == None) or (maxMult > 1):
@ -866,6 +873,10 @@ class AbstractMixin:
# I create a new entry "backd"; if I put the dict in "back" I # 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 # really modify the initial appyType object and I don't want to do
# this. # 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 return res
def _appy_getAtType(self, appyClass, flavour=None): def _appy_getAtType(self, appyClass, flavour=None):
@ -979,6 +990,26 @@ class AbstractMixin:
res.append( (v, self.utranslate(labels[i], domain=domain))) res.append( (v, self.utranslate(labels[i], domain=domain)))
return self.getProductConfig().DisplayList(tuple(res)) 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, '', ' ') nullValues = (None, '', ' ')
numbersMap = {'Integer': 'int', 'Float': 'float'} numbersMap = {'Integer': 'int', 'Float': 'float'}
validatorTypes = (types.FunctionType, type(String.EMAIL)) validatorTypes = (types.FunctionType, type(String.EMAIL))

View file

@ -7,7 +7,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import copy, types 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: class ModelClass:
@ -22,13 +22,14 @@ class ModelClass:
# instance of a ModelClass, those attributes must not be # instance of a ModelClass, those attributes must not be
# given in the constructor. # given in the constructor.
@classmethod
def _appy_addField(klass, fieldName, fieldType, classDescr): def _appy_addField(klass, fieldName, fieldType, classDescr):
exec "klass.%s = fieldType" % fieldName exec "klass.%s = fieldType" % fieldName
klass._appy_attributes.append(fieldName) klass._appy_attributes.append(fieldName)
if hasattr(klass, '_appy_classes'): if hasattr(klass, '_appy_classes'):
klass._appy_classes[fieldName] = classDescr.name klass._appy_classes[fieldName] = classDescr.name
_appy_addField = classmethod(_appy_addField)
@classmethod
def _appy_getTypeBody(klass, appyType): def _appy_getTypeBody(klass, appyType):
'''This method returns the code declaration for p_appyType.''' '''This method returns the code declaration for p_appyType.'''
typeArgs = '' typeArgs = ''
@ -45,10 +46,12 @@ class ModelClass:
attrValue = attrValue.__name__ attrValue = attrValue.__name__
else: else:
attrValue = '%s.%s' % (moduleName, attrValue.__name__) attrValue = '%s.%s' % (moduleName, attrValue.__name__)
elif isinstance(attrValue, Selection):
attrValue = 'Selection("%s")' % attrValue.methodName
typeArgs += '%s=%s,' % (attrName, attrValue) typeArgs += '%s=%s,' % (attrName, attrValue)
return '%s(%s)' % (appyType.__class__.__name__, typeArgs) return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
_appy_getTypeBody = classmethod(_appy_getTypeBody)
@classmethod
def _appy_getBody(klass): def _appy_getBody(klass):
'''This method returns the code declaration of this class. We will dump '''This method returns the code declaration of this class. We will dump
this in appyWrappers.py in the resulting product.''' this in appyWrappers.py in the resulting product.'''
@ -57,7 +60,6 @@ class ModelClass:
exec 'appyType = klass.%s' % attrName exec 'appyType = klass.%s' % attrName
res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType)) res += ' %s=%s\n' % (attrName, klass._appy_getTypeBody(appyType))
return res return res
_appy_getBody = classmethod(_appy_getBody)
class PodTemplate(ModelClass): class PodTemplate(ModelClass):
description = String(format=String.TEXT) description = String(format=String.TEXT)
@ -87,6 +89,7 @@ class Flavour(ModelClass):
# We need to remember the original classes related to the flavour attributes # We need to remember the original classes related to the flavour attributes
_appy_attributes = list(defaultFlavourAttrs) _appy_attributes = list(defaultFlavourAttrs)
@classmethod
def _appy_clean(klass): def _appy_clean(klass):
toClean = [] toClean = []
for k, v in klass.__dict__.iteritems(): for k, v in klass.__dict__.iteritems():
@ -97,8 +100,8 @@ class Flavour(ModelClass):
exec 'del klass.%s' % k exec 'del klass.%s' % k
klass._appy_attributes = list(defaultFlavourAttrs) klass._appy_attributes = list(defaultFlavourAttrs)
klass._appy_classes = {} klass._appy_classes = {}
_appy_clean = classmethod(_appy_clean)
@classmethod
def _appy_copyField(klass, appyType): def _appy_copyField(klass, appyType):
'''From a given p_appyType, produce a type definition suitable for '''From a given p_appyType, produce a type definition suitable for
storing the default value for this field.''' storing the default value for this field.'''
@ -121,8 +124,8 @@ class Flavour(ModelClass):
res.back.show = False res.back.show = False
res.select = None # Not callable from flavour res.select = None # Not callable from flavour
return res return res
_appy_copyField = classmethod(_appy_copyField)
@classmethod
def _appy_addOptionalField(klass, fieldDescr): def _appy_addOptionalField(klass, fieldDescr):
className = fieldDescr.classDescr.name className = fieldDescr.classDescr.name
fieldName = 'optionalFieldsFor%s' % className fieldName = 'optionalFieldsFor%s' % className
@ -134,8 +137,8 @@ class Flavour(ModelClass):
fieldType.validator.append(fieldDescr.fieldName) fieldType.validator.append(fieldDescr.fieldName)
fieldType.page = 'data' fieldType.page = 'data'
fieldType.group = fieldDescr.classDescr.klass.__name__ fieldType.group = fieldDescr.classDescr.klass.__name__
_appy_addOptionalField = classmethod(_appy_addOptionalField)
@classmethod
def _appy_addDefaultField(klass, fieldDescr): def _appy_addDefaultField(klass, fieldDescr):
className = fieldDescr.classDescr.name className = fieldDescr.classDescr.name
fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName) fieldName = 'defaultValueFor%s_%s' % (className, fieldDescr.fieldName)
@ -143,8 +146,8 @@ class Flavour(ModelClass):
klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr) klass._appy_addField(fieldName, fieldType, fieldDescr.classDescr)
fieldType.page = 'data' fieldType.page = 'data'
fieldType.group = fieldDescr.classDescr.klass.__name__ fieldType.group = fieldDescr.classDescr.klass.__name__
_appy_addDefaultField = classmethod(_appy_addDefaultField)
@classmethod
def _appy_addPodField(klass, classDescr): def _appy_addPodField(klass, classDescr):
'''Adds a POD field to the flavour and also an integer field that will '''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 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) klass._appy_addField(fieldName, fieldType, classDescr)
classDescr.flavourFieldsToPropagate.append( classDescr.flavourFieldsToPropagate.append(
('podMaxShownTemplatesFor%s', copy.copy(fieldType)) ) ('podMaxShownTemplatesFor%s', copy.copy(fieldType)) )
_appy_addPodField = classmethod(_appy_addPodField)
@classmethod
def _appy_addQueryResultColumns(klass, classDescr): 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 className = classDescr.name
fieldName = 'resultColumnsFor%s' % className fieldName = 'resultColumnsFor%s' % className
attrNames = [a[0] for a in classDescr.getOrderedAppyAttributes()] fieldType = String(multiplicity=(0,None), validator=Selection(
attrNames.append('workflowState') # Object state from workflow '_appy_getAllFields*%s' % className), page='userInterface',
if 'title' in attrNames:
attrNames.remove('title') # Included by default.
fieldType = String(multiplicity=(0,None), validator=attrNames,
page='userInterface',
group=classDescr.klass.__name__) group=classDescr.klass.__name__)
klass._appy_addField(fieldName, fieldType, classDescr) 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): def _appy_addWorkflowFields(klass, classDescr):
'''Adds, for a given p_classDescr, the workflow-related fields.''' '''Adds, for a given p_classDescr, the workflow-related fields.'''
className = classDescr.name className = classDescr.name
@ -209,8 +238,6 @@ class Flavour(ModelClass):
group=groupName) group=groupName)
klass._appy_addField(fieldName, fieldType, classDescr) klass._appy_addField(fieldName, fieldType, classDescr)
_appy_addWorkflowFields = classmethod(_appy_addWorkflowFields)
class Tool(ModelClass): class Tool(ModelClass):
flavours = Ref(None, multiplicity=(1,None), add=True, link=False, flavours = Ref(None, multiplicity=(1,None), add=True, link=False,
back=Ref(attribute='tool')) back=Ref(attribute='tool'))

View file

@ -647,7 +647,8 @@
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**')"> 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"> <tal:result condition="objs">
@ -655,6 +656,10 @@
<legend> <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="structure python: test(searchName, tool.translate(searchLabel), test(severalTypes, tool.translate(tool.getAppName()), tool.translate('%s_plural' % contentType)))"/>
(<span tal:replace="totalNumber"/>) (<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> </legend>
<table cellpadding="0" cellspacing="0" width="100%"><tr> <table cellpadding="0" cellspacing="0" width="100%"><tr>
@ -787,9 +792,13 @@
</fieldset> </fieldset>
</tal:result> </tal:result>
<span tal:condition="not: objs" <tal:noResult condition="not: objs">
tal:content="python: tool.translate('query_no_result')">No result. <span tal:replace="python: tool.translate('query_no_result')"/>
</span> <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> </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">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: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> <tal:comment replace="nothing">Section title, with action icons</tal:comment>
<dt tal:attributes="class python:test(repeat['rootClass'].number()==1, 'portletAppyItem', 'portletAppyItem portletSep')"> <dt tal:attributes="class python:test(repeat['rootClass'].number()==1, 'portletAppyItem', 'portletAppyItem portletSep')">
<table width="100%" cellspacing="0" cellpadding="0" class="no-style-table"> <table width="100%" cellspacing="0" cellpadding="0" class="no-style-table">
@ -911,6 +922,8 @@
title python: tool.translate('query_import')"/> title python: tool.translate('query_import')"/>
<tal:comment replace="nothing">Search objects of this type (todo: update flavourNumber)</tal:comment> <tal:comment replace="nothing">Search objects of this type (todo: update flavourNumber)</tal:comment>
<img style="cursor:pointer" <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); 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; src string: $portal_url/skyn/search.gif;
title python: tool.translate('search_objects')"/> title python: tool.translate('search_objects')"/>

View file

@ -19,7 +19,7 @@
<table class="no-style-table" cellpadding="0" cellspacing="0"> <table class="no-style-table" cellpadding="0" cellspacing="0">
<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)&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); <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()})"> 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> <tal:comment replace="nothing">Move up</tal:comment>

View file

@ -16,10 +16,12 @@
tal:define="appFolder context/getParentNode; tal:define="appFolder context/getParentNode;
contentType request/type_name; contentType request/type_name;
tool python: portal.get('portal_%s' % appFolder.id.lower()); 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> <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> <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"> <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="type_name" tal:attributes="value contentType"/>
<input type="hidden" name="flavourNumber:int" tal:attributes="value request/flavourNumber"/> <input type="hidden" name="flavourNumber:int" tal:attributes="value request/flavourNumber"/>
<tal:searchFields repeat="searchField searchableFields"> <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]; <tal:showSearchField define="fieldName python:searchField[0];
appyType python:searchField[1]"> appyType python:searchField[1];
widgetName python: 'w_%s' % fieldName">
<metal:searchField use-macro="python: appFolder.skyn.widgets.macros.get('search%s' % searchField[1]['type'])"/> <metal:searchField use-macro="python: appFolder.skyn.widgets.macros.get('search%s' % searchField[1]['type'])"/>
</tal:showSearchField> </tal:showSearchField>
</tal:searchFields> </tal:field><br/><br class="discreet"/>
</td>
</tr>
</table>
<tal:comment replace="nothing">Submit button</tal:comment> <tal:comment replace="nothing">Submit button</tal:comment>
<p align="right"><br/> <p align="right"><br/>

View file

@ -7,11 +7,35 @@
</metal:searchFloat> </metal:searchFloat>
<metal:searchString define-macro="searchString"> <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:searchString>
<metal:searchBoolean define-macro="searchBoolean"> <metal:searchBoolean define-macro="searchBoolean" tal:define="typedWidget python:'%s*bool' % widgetName">
<p tal:content="fieldName">Hello</p> <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:searchBoolean>
<metal:searchDate define-macro="searchDate"> <metal:searchDate define-macro="searchDate">

View file

@ -42,4 +42,8 @@ referers = {
# names of DC transitions. # names of DC transitions.
workflowInstances = {} workflowInstances = {}
<!workflowInstancesInit!> <!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 Stores the list of columns that must be show when displaying
instances of the a given root p_klass. 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" "optionalFields"
Stores the list of optional attributes that are in use in the Stores the list of optional attributes that are in use in the
current flavour for the given p_klass. current flavour for the given p_klass.

View file

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