diff --git a/gen/__init__.py b/gen/__init__.py index ca4d2bb..f17fed0 100755 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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: diff --git a/gen/descriptors.py b/gen/descriptors.py index 11a53ca..238b84c 100644 --- a/gen/descriptors.py +++ b/gen/descriptors.py @@ -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 diff --git a/gen/plone25/descriptors.py b/gen/plone25/descriptors.py index 96da06e..d77a4c3 100644 --- a/gen/plone25/descriptors.py +++ b/gen/plone25/descriptors.py @@ -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, diff --git a/gen/plone25/generator.py b/gen/plone25/generator.py index a55674e..f070577 100644 --- a/gen/plone25/generator.py +++ b/gen/plone25/generator.py @@ -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 diff --git a/gen/plone25/mixins/FlavourMixin.py b/gen/plone25/mixins/FlavourMixin.py index dc5986f..c06e4b1 100644 --- a/gen/plone25/mixins/FlavourMixin.py +++ b/gen/plone25/mixins/FlavourMixin.py @@ -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 # ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/ToolMixin.py b/gen/plone25/mixins/ToolMixin.py index 20299b1..7e8de99 100644 --- a/gen/plone25/mixins/ToolMixin.py +++ b/gen/plone25/mixins/ToolMixin.py @@ -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 # ------------------------------------------------------------------------------ diff --git a/gen/plone25/mixins/__init__.py b/gen/plone25/mixins/__init__.py index 2713159..ad15016 100644 --- a/gen/plone25/mixins/__init__.py +++ b/gen/plone25/mixins/__init__.py @@ -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)) diff --git a/gen/plone25/model.py b/gen/plone25/model.py index 79552bd..f7fdf8a 100644 --- a/gen/plone25/model.py +++ b/gen/plone25/model.py @@ -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) @@ -86,7 +88,8 @@ class Flavour(ModelClass): _appy_classes = {} # ~{s_attributeName: s_className}~ # 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 @@ -208,8 +237,6 @@ class Flavour(ModelClass): fieldType = Boolean(default=defaultValue, page='userInterface', 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, diff --git a/gen/plone25/skin/macros.pt b/gen/plone25/skin/macros.pt index 9e4e297..4bb47d1 100644 --- a/gen/plone25/skin/macros.pt +++ b/gen/plone25/skin/macros.pt @@ -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);"> @@ -655,6 +656,10 @@ () + +   —   + @@ -787,9 +792,13 @@ - No result. - + + + +
+
+
@@ -883,7 +892,9 @@ TODO: implement a widget for selecting the needed flavour.Create a section for every root class. - + Section title, with action icons
@@ -911,6 +922,8 @@ title python: tool.translate('query_import')"/> Search objects of this type (todo: update flavourNumber) diff --git a/gen/plone25/skin/ref.pt b/gen/plone25/skin/ref.pt index 5129be3..cfb5367 100644 --- a/gen/plone25/skin/ref.pt +++ b/gen/plone25/skin/ref.pt @@ -19,7 +19,7 @@
Arrows for moving objects up or down -
+ Move up diff --git a/gen/plone25/skin/search.pt b/gen/plone25/skin/search.pt index 24c3091..5c9797b 100644 --- a/gen/plone25/skin/search.pt +++ b/gen/plone25/skin/search.pt @@ -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)"> Search title -


+

— +


Form for searching objects of request/type_name.
@@ -27,12 +29,20 @@ - - - - - + + + + +
+ + + + +

+
Submit button


diff --git a/gen/plone25/skin/widgets.pt b/gen/plone25/skin/widgets.pt index 5d37efc..042cf16 100644 --- a/gen/plone25/skin/widgets.pt +++ b/gen/plone25/skin/widgets.pt @@ -7,11 +7,35 @@ -

Hello

+
   + Show a simple search field for most String fields. + + + + Show a multi-selection box for fields whose validator defines a list of values. + + + - -

Hello

+ +
   + + + + + + + + + + + +
diff --git a/gen/plone25/templates/config.py b/gen/plone25/templates/config.py index 032275f..f026998 100644 --- a/gen/plone25/templates/config.py +++ b/gen/plone25/templates/config.py @@ -42,4 +42,8 @@ referers = { # names of DC transitions. workflowInstances = {} + +# In the following dict, we store, for every Appy class, the ordered list of +# attributes (included inherited attributes). +attributes = {} # ------------------------------------------------------------------------------ diff --git a/gen/plone25/wrappers/FlavourWrapper.py b/gen/plone25/wrappers/FlavourWrapper.py index bbd7509..c851a42 100644 --- a/gen/plone25/wrappers/FlavourWrapper.py +++ b/gen/plone25/wrappers/FlavourWrapper.py @@ -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. diff --git a/gen/po.py b/gen/po.py index 6c24ffb..93d229c 100755 --- a/gen/po.py +++ b/gen/po.py @@ -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