diff --git a/gen/__init__.py b/gen/__init__.py index d3f50d0..58a23ca 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -317,6 +317,7 @@ class Search: # In the dict below, keys are indexed field names and values are # search values. self.fields = fields + @staticmethod def getIndexName(fieldName, usage='search'): '''Gets the name of the technical index that corresponds to field named @@ -333,13 +334,15 @@ class Search: elif fieldName in defaultIndexes: return fieldName else: return 'get%s%s'% (fieldName[0].upper(),fieldName[1:]) + @staticmethod def getSearchValue(fieldName, fieldValue): '''Returns a transformed p_fieldValue for producing a valid search value as required for searching in the index corresponding to p_fieldName.''' - if fieldName == 'title': - # Title is a TextIndex. We must split p_fieldValue into keywords. + if fieldName in ('title', 'SearchableText'): + # Title and SearchableText are TextIndex indexes. We must split + # p_fieldValue into keywords. res = Keywords(fieldValue).get() elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'): v = fieldValue[:-1] @@ -366,6 +369,38 @@ class Search: res = fieldValue return res + def updateSearchCriteria(self, criteria, advanced=False): + '''This method updates dict p_criteria with all the search criteria + corresponding to this Search instance. If p_advanced is True, + p_criteria correspond to an advanced search, to be stored in the + session: in this case we need to keep the Appy names for parameters + sortBy and sortOrder (and not "resolve" them to Zope's sort_on and + sort_order).''' + # Put search criteria in p_criteria + for fieldName, fieldValue in self.fields.iteritems(): + # Management of searches restricted to objects linked through a + # Ref field: not implemented yet. + if fieldName == '_ref': continue + # Make the correspondence between the name of the field and the + # name of the corresponding index, excepted if advanced is True: in + # that case, the correspondence will be done later. + if not advanced: + attrName = Search.getIndexName(fieldName) + # Express the field value in the way needed by the index + criteria[attrName]= Search.getSearchValue(fieldName, fieldValue) + else: + criteria[fieldName]= fieldValue + # Add a sort order if specified + if self.sortBy: + if not advanced: + criteria['sort_on'] = Search.getIndexName(self.sortBy, + usage='sort') + if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse' + else: criteria['sort_order'] = None + else: + criteria['sortBy'] = self.sortBy + criteria['sortOrder'] = self.sortOrder + # ------------------------------------------------------------------------------ class Type: '''Basic abstract class for defining any appy type.''' diff --git a/gen/installer.py b/gen/installer.py index 22e65d4..93c03a3 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -176,6 +176,12 @@ class ZopeInstaller: wrapperClass = tool.getAppyClass(className, wrapper=True) indexInfo.update(wrapperClass.getIndexes(includeDefaults=False)) updateIndexes(self, indexInfo) + # Re-index index "SearchableText", wrongly defined for Appy < 0.8.3. + stIndex = catalog.Indexes['SearchableText'] + if stIndex.indexSize() == 0: + self.logger.info('Reindexing SearchableText...') + catalog.reindexIndex('SearchableText', self.app.REQUEST) + self.logger.info('Done.') def getAddPermission(self, className): '''What is the name of the permission allowing to create instances of @@ -364,7 +370,8 @@ class ZopeInstaller: wrapperClass = klass.wrapperClass if not hasattr(wrapperClass, 'title'): # Special field "type" is mandatory for every class. - title = gen.String(multiplicity=(1,1),show='edit',indexed=True) + title = gen.String(multiplicity=(1,1), show='edit', + indexed=True, searchable=True) title.init('title', None, 'appy') setattr(wrapperClass, 'title', title) # Special field "state" must be added for every class diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index ffa7330..5eebb63 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -337,22 +337,8 @@ class ToolMixin(BaseMixin): # Manage additional criteria from a search when relevant if searchName: search = self.getSearch(className, searchName) if search: - # Add additional search criteria - for fieldName, fieldValue in search.fields.iteritems(): - # Management of searches restricted to objects linked through a - # Ref field: not implemented yet. - if fieldName == '_ref': continue - # Make the correspondance between the name of the field and the - # name of the corresponding index. - attrName = Search.getIndexName(fieldName) - # Express the field value in the way needed by the index - params[attrName] = Search.getSearchValue(fieldName, fieldValue) - # Add a sort order if specified - sortKey = search.sortBy - if sortKey: - params['sort_on'] = Search.getIndexName(sortKey, usage='sort') - if search.sortOrder == 'desc': params['sort_order'] = 'reverse' - else: params['sort_order'] = None + # Add in params search and sort criteria. + search.updateSearchCriteria(params) # Determine or override sort if specified. if sortBy: params['sort_on'] = Search.getIndexName(sortBy, usage='sort') @@ -587,6 +573,21 @@ class ToolMixin(BaseMixin): day = int(day)-1 return res + def _getDefaultSearchCriteria(self): + '''We are about to perform an advanced search on instances of a given + class. Check, on this class, if in field Class.searchAdvanced, some + default criteria (field values, sort filters, etc) exist, and, if + yes, return it.''' + res = {} + rq = self.REQUEST + if 'className' not in rq.form: return res + klass = self.getAppyClass(rq.form['className']) + if not hasattr(klass, 'searchAdvanced'): return res + # In this attribute, we have the Search instance representing automatic + # advanced search criteria. + klass.searchAdvanced.updateSearchCriteria(res, advanced=True) + return res + transformMethods = {'uppercase': 'upper', 'lowercase': 'lower', 'capitalize': 'capitalize'} def onSearchObjects(self): @@ -594,7 +595,7 @@ class ToolMixin(BaseMixin): search.pt.''' rq = self.REQUEST # Store the search criteria in the session - criteria = {} + criteria = self._getDefaultSearchCriteria() for attrName in rq.form.keys(): if attrName.startswith('w_') and \ not self._searchValueIsEmpty(attrName): diff --git a/gen/model.py b/gen/model.py index 97d670b..9297fbc 100644 --- a/gen/model.py +++ b/gen/model.py @@ -265,7 +265,8 @@ class Tool(ModelClass): # Document generation page dgp = {'page': gen.Page('documents', show=isManagerEdit)} def validPythonWithUno(self, value): pass # Real method in the wrapper - unoEnabledPython = gen.String(show=False,validator=validPythonWithUno,**dgp) + unoEnabledPython = gen.String(default='/usr/bin/python', show=False, + validator=validPythonWithUno, **dgp) openOfficePort = gen.Integer(default=2002, show=False, **dgp) # User interface page numberOfResultsPerPage = gen.Integer(default=30, diff --git a/gen/po.py b/gen/po.py index f4301b1..ce92075 100644 --- a/gen/po.py +++ b/gen/po.py @@ -52,7 +52,6 @@ appyLabels = [ ('import_done', 'Import terminated successfully.'), ('search_title', 'Advanced search'), ('search_button', 'Search'), - ('search_objects', 'Search objects of this type.'), ('search_results', 'Search results'), ('search_results_descr', ' '), ('search_new', 'New search'), @@ -108,8 +107,8 @@ appyLabels = [ ('app_home', 'Home'), ('login_reserved', 'This login is reserved.'), ('login_in_use', 'This login is already in use.'), - ('login_ko', 'Welcome! You are now logged in.'), - ('login_ok', 'Login failed.'), + ('login_ko', 'Login failed.'), + ('login_ok', 'Welcome! You are now logged in.'), ('password_too_short', 'Passwords must contain at least ${nb} characters.'), ('passwords_mismatch', 'Passwords do not match.'), ('object_save', 'Save'), diff --git a/gen/ui/portlet.pt b/gen/ui/portlet.pt index 90b5a46..44c2a8e 100644 --- a/gen/ui/portlet.pt +++ b/gen/ui/portlet.pt @@ -50,6 +50,7 @@ appUrl app/absolute_url; currentSearch req/search|nothing; currentClass req/className|nothing; + currentPage python: req['PATH_INFO'].rsplit('/',1)[-1]; rootClasses tool/getRootClasses; phases python: contextObj and contextObj.getAppyPhases() or None"> @@ -68,12 +69,12 @@ Section title (link triggers the default search), with action icons - + Create a new object from a web form - Search objects of this type - - - - - Searches for this content type + +
+ + + Live search +
+ + + + + + + +
+
+ Advanced search +
+ + ... + +
+
+ Predefined searches diff --git a/gen/ui/widgets/ref.pt b/gen/ui/widgets/ref.pt index e8434e8..40f5b49 100644 --- a/gen/ui/widgets/ref.pt +++ b/gen/ui/widgets/ref.pt @@ -158,7 +158,7 @@ The search icon if field is queryable - + Appy (top) navigation diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 578e0de..194cbf9 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -97,7 +97,7 @@ class ToolWrapper(AbstractWrapper): "resultColumns" Stores the list of columns that must be shown when displaying - instances of the a given root p_klass. + instances of a given root p_klass. "enableAdvancedSearch" Determines if the advanced search screen must be enabled for diff --git a/pod/doc_importers.py b/pod/doc_importers.py index 623af46..480b982 100644 --- a/pod/doc_importers.py +++ b/pod/doc_importers.py @@ -174,6 +174,8 @@ pxToCm = 44.173513561 def getSize(filePath, fileType): '''Gets the size of an image by reading first bytes.''' x, y = (None, None) + # Get fileType from filePath if not given. + if not fileType: fileType = os.path.splitext(filePath)[1][1:] f = file(filePath, 'rb') if fileType in jpgTypes: # Dummy read to skip header ID