Improved advanced search functionality + completed String fields with Selection instance as validator.
This commit is contained in:
parent
f8baeee4f7
commit
d6607d7815
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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'">
|
||||
— <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')"/>
|
||||
|
|
|
@ -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)>1) and member.has_permission('Modify portal content', obj)">
|
||||
<td class="noPadding" tal:condition="python: (len(objs)>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>
|
||||
|
|
|
@ -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/>
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
<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">
|
||||
|
|
|
@ -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!>}
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue