[gen] Improved search abilities. [pod] Bugfix: import several times an image from the same URL.

This commit is contained in:
Gaetan Delannay 2013-02-18 15:03:26 +01:00
parent 2307a284cc
commit 24089ef674
9 changed files with 103 additions and 39 deletions

View file

@ -317,6 +317,7 @@ class Search:
# In the dict below, keys are indexed field names and values are # In the dict below, keys are indexed field names and values are
# search values. # search values.
self.fields = fields self.fields = fields
@staticmethod @staticmethod
def getIndexName(fieldName, usage='search'): def getIndexName(fieldName, usage='search'):
'''Gets the name of the technical index that corresponds to field named '''Gets the name of the technical index that corresponds to field named
@ -333,13 +334,15 @@ class Search:
elif fieldName in defaultIndexes: return fieldName elif fieldName in defaultIndexes: return fieldName
else: else:
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:]) return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
@staticmethod @staticmethod
def getSearchValue(fieldName, fieldValue): def getSearchValue(fieldName, fieldValue):
'''Returns a transformed p_fieldValue for producing a valid search '''Returns a transformed p_fieldValue for producing a valid search
value as required for searching in the index corresponding to value as required for searching in the index corresponding to
p_fieldName.''' p_fieldName.'''
if fieldName == 'title': if fieldName in ('title', 'SearchableText'):
# Title is a TextIndex. We must split p_fieldValue into keywords. # Title and SearchableText are TextIndex indexes. We must split
# p_fieldValue into keywords.
res = Keywords(fieldValue).get() res = Keywords(fieldValue).get()
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'): elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
v = fieldValue[:-1] v = fieldValue[:-1]
@ -366,6 +369,38 @@ class Search:
res = fieldValue res = fieldValue
return res 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: class Type:
'''Basic abstract class for defining any appy type.''' '''Basic abstract class for defining any appy type.'''

View file

@ -176,6 +176,12 @@ class ZopeInstaller:
wrapperClass = tool.getAppyClass(className, wrapper=True) wrapperClass = tool.getAppyClass(className, wrapper=True)
indexInfo.update(wrapperClass.getIndexes(includeDefaults=False)) indexInfo.update(wrapperClass.getIndexes(includeDefaults=False))
updateIndexes(self, indexInfo) 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): def getAddPermission(self, className):
'''What is the name of the permission allowing to create instances of '''What is the name of the permission allowing to create instances of
@ -364,7 +370,8 @@ class ZopeInstaller:
wrapperClass = klass.wrapperClass wrapperClass = klass.wrapperClass
if not hasattr(wrapperClass, 'title'): if not hasattr(wrapperClass, 'title'):
# Special field "type" is mandatory for every class. # 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') title.init('title', None, 'appy')
setattr(wrapperClass, 'title', title) setattr(wrapperClass, 'title', title)
# Special field "state" must be added for every class # Special field "state" must be added for every class

View file

@ -337,22 +337,8 @@ class ToolMixin(BaseMixin):
# Manage additional criteria from a search when relevant # Manage additional criteria from a search when relevant
if searchName: search = self.getSearch(className, searchName) if searchName: search = self.getSearch(className, searchName)
if search: if search:
# Add additional search criteria # Add in params search and sort criteria.
for fieldName, fieldValue in search.fields.iteritems(): search.updateSearchCriteria(params)
# 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
# Determine or override sort if specified. # Determine or override sort if specified.
if sortBy: if sortBy:
params['sort_on'] = Search.getIndexName(sortBy, usage='sort') params['sort_on'] = Search.getIndexName(sortBy, usage='sort')
@ -587,6 +573,21 @@ class ToolMixin(BaseMixin):
day = int(day)-1 day = int(day)-1
return res 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', transformMethods = {'uppercase': 'upper', 'lowercase': 'lower',
'capitalize': 'capitalize'} 'capitalize': 'capitalize'}
def onSearchObjects(self): def onSearchObjects(self):
@ -594,7 +595,7 @@ class ToolMixin(BaseMixin):
search.pt.''' search.pt.'''
rq = self.REQUEST rq = self.REQUEST
# Store the search criteria in the session # Store the search criteria in the session
criteria = {} criteria = self._getDefaultSearchCriteria()
for attrName in rq.form.keys(): for attrName in rq.form.keys():
if attrName.startswith('w_') and \ if attrName.startswith('w_') and \
not self._searchValueIsEmpty(attrName): not self._searchValueIsEmpty(attrName):

View file

@ -265,7 +265,8 @@ class Tool(ModelClass):
# Document generation page # Document generation page
dgp = {'page': gen.Page('documents', show=isManagerEdit)} dgp = {'page': gen.Page('documents', show=isManagerEdit)}
def validPythonWithUno(self, value): pass # Real method in the wrapper 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) openOfficePort = gen.Integer(default=2002, show=False, **dgp)
# User interface page # User interface page
numberOfResultsPerPage = gen.Integer(default=30, numberOfResultsPerPage = gen.Integer(default=30,

View file

@ -52,7 +52,6 @@ appyLabels = [
('import_done', 'Import terminated successfully.'), ('import_done', 'Import terminated successfully.'),
('search_title', 'Advanced search'), ('search_title', 'Advanced search'),
('search_button', 'Search'), ('search_button', 'Search'),
('search_objects', 'Search objects of this type.'),
('search_results', 'Search results'), ('search_results', 'Search results'),
('search_results_descr', ' '), ('search_results_descr', ' '),
('search_new', 'New search'), ('search_new', 'New search'),
@ -108,8 +107,8 @@ appyLabels = [
('app_home', 'Home'), ('app_home', 'Home'),
('login_reserved', 'This login is reserved.'), ('login_reserved', 'This login is reserved.'),
('login_in_use', 'This login is already in use.'), ('login_in_use', 'This login is already in use.'),
('login_ko', 'Welcome! You are now logged in.'), ('login_ko', 'Login failed.'),
('login_ok', 'Login failed.'), ('login_ok', 'Welcome! You are now logged in.'),
('password_too_short', 'Passwords must contain at least ${nb} characters.'), ('password_too_short', 'Passwords must contain at least ${nb} characters.'),
('passwords_mismatch', 'Passwords do not match.'), ('passwords_mismatch', 'Passwords do not match.'),
('object_save', 'Save'), ('object_save', 'Save'),

View file

@ -50,6 +50,7 @@
appUrl app/absolute_url; appUrl app/absolute_url;
currentSearch req/search|nothing; currentSearch req/search|nothing;
currentClass req/className|nothing; currentClass req/className|nothing;
currentPage python: req['PATH_INFO'].rsplit('/',1)[-1];
rootClasses tool/getRootClasses; rootClasses tool/getRootClasses;
phases python: contextObj and contextObj.getAppyPhases() or None"> phases python: contextObj and contextObj.getAppyPhases() or None">
@ -68,10 +69,10 @@
<tal:comment replace="nothing">Section title (link triggers the default search), with action icons</tal:comment> <tal:comment replace="nothing">Section title (link triggers the default search), with action icons</tal:comment>
<a tal:define="queryParam python: searchInfo['default'] and searchInfo['default']['name'] or ''" <a tal:define="queryParam python: searchInfo['default'] and searchInfo['default']['name'] or ''"
tal:attributes="href python: '%s?className=%s&search=%s' % (queryUrl, rootClass, queryParam); tal:attributes="href python: '%s?className=%s&search=%s' % (queryUrl, rootClass, queryParam);
class python:test(not currentSearch and (currentClass==rootClass), 'portletCurrent', '')" class python: (not currentSearch and (currentClass==rootClass) and (currentPage=='query')) and 'portletCurrent' or ''"
tal:content="structure python: _(rootClass + '_plural')"> tal:content="structure python: _(rootClass + '_plural')">
</a> </a>
<span tal:define="addPermission python: '%s: Add %s' % (appName, rootClass); <tal:icons define="addPermission python: '%s: Add %s' % (appName, rootClass);
userMayAdd python: user.has_permission(addPermission, appFolder); userMayAdd python: user.has_permission(addPermission, appFolder);
createMeans python: tool.getCreateMeans(rootClass)"> createMeans python: tool.getCreateMeans(rootClass)">
<tal:comment replace="nothing">Create a new object from a web form</tal:comment> <tal:comment replace="nothing">Create a new object from a web form</tal:comment>
@ -86,15 +87,33 @@
title python: _('query_import')"> title python: _('query_import')">
<img tal:attributes="src string: $appUrl/ui/import.png"/> <img tal:attributes="src string: $appUrl/ui/import.png"/>
</a> </a>
<tal:comment replace="nothing">Search objects of this type</tal:comment> </tal:icons>
<a tal:define="showSearch python: tool.getAttr('enableAdvancedSearchFor%s' % rootClass)" <br/>
tal:condition="showSearch"
tal:attributes="href python: '%s/ui/search?className=%s' % (toolUrl, rootClass); <tal:search condition="python: tool.getAttr('enableAdvancedSearchFor%s' % rootClass)">
title python: _('search_objects')"> <tal:comment replace="nothing">Live search</tal:comment>
<img tal:attributes="src string: $appUrl/ui/search.gif"/> <form tal:attributes="action string: $appUrl/config/do">
<input type="hidden" name="action" value="SearchObjects"/>
<input type="hidden" name="className" tal:attributes="value rootClass"/>
<table cellpadding="0" cellspacing="0">
<tr valign="middle">
<td style="padding-right: 3px"><input type="text" size="14" name="w_SearchableText"/></td>
<td><input type="image" style="cursor:pointer"
tal:attributes="src string: $appUrl/ui/search.gif;
title python: _('search_button')"/></td>
</tr>
</table>
</form>
<tal:comment replace="nothing">Advanced search</tal:comment>
<div tal:define="highlighted python: (currentClass == rootClass) and (currentPage == 'search')"
tal:attributes="class python: highlighted and 'portletSearch portletCurrent' or 'portletSearch'">
<a tal:define="text python: _('search_title')"
tal:attributes="href python: '%s/ui/search?className=%s' % (toolUrl, rootClass); title text">
<span tal:replace="text"/>...
</a> </a>
</span> </div>
<tal:comment replace="nothing">Searches for this content type</tal:comment> </tal:search>
<tal:comment replace="nothing">Predefined searches</tal:comment>
<tal:widget repeat="widget searchInfo/searches"> <tal:widget repeat="widget searchInfo/searches">
<tal:group condition="python: widget['type'] == 'group'"> <tal:group condition="python: widget['type'] == 'group'">
<metal:s use-macro="app/ui/portlet/macros/group"/> <metal:s use-macro="app/ui/portlet/macros/group"/>

View file

@ -158,7 +158,7 @@
<tal:comment replace="nothing">The search icon if field is queryable</tal:comment> <tal:comment replace="nothing">The search icon if field is queryable</tal:comment>
<a tal:condition="python: objs and appyType['queryable']" <a tal:condition="python: objs and appyType['queryable']"
tal:attributes="href python: '%s/ui/search?className=%s&ref=%s:%s' % (tool.absolute_url(), linkedPortalType, contextObj.UID(), appyType['name'])"> tal:attributes="href python: '%s/ui/search?className=%s&ref=%s:%s' % (tool.absolute_url(), linkedPortalType, contextObj.UID(), appyType['name'])">
<img src="search.gif" tal:attributes="title python: _('search_objects')"/></a> <img src="search.gif" tal:attributes="title python: _('search_title')"/></a>
</div> </div>
<tal:comment replace="nothing">Appy (top) navigation</tal:comment> <tal:comment replace="nothing">Appy (top) navigation</tal:comment>

View file

@ -97,7 +97,7 @@ class ToolWrapper(AbstractWrapper):
"resultColumns" "resultColumns"
Stores the list of columns that must be shown when displaying 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" "enableAdvancedSearch"
Determines if the advanced search screen must be enabled for Determines if the advanced search screen must be enabled for

View file

@ -174,6 +174,8 @@ pxToCm = 44.173513561
def getSize(filePath, fileType): def getSize(filePath, fileType):
'''Gets the size of an image by reading first bytes.''' '''Gets the size of an image by reading first bytes.'''
x, y = (None, None) 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') f = file(filePath, 'rb')
if fileType in jpgTypes: if fileType in jpgTypes:
# Dummy read to skip header ID # Dummy read to skip header ID