[gen] Improved search abilities. [pod] Bugfix: import several times an image from the same URL.
This commit is contained in:
parent
2307a284cc
commit
24089ef674
|
@ -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.'''
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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,12 +69,12 @@
|
||||||
<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>
|
||||||
<a tal:condition="python: ('form' in createMeans) and userMayAdd"
|
<a tal:condition="python: ('form' in createMeans) and userMayAdd"
|
||||||
tal:attributes="href python: '%s/do?action=Create&className=%s' % (toolUrl, rootClass);
|
tal:attributes="href python: '%s/do?action=Create&className=%s' % (toolUrl, rootClass);
|
||||||
|
@ -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">
|
||||||
</a>
|
<input type="hidden" name="action" value="SearchObjects"/>
|
||||||
</span>
|
<input type="hidden" name="className" tal:attributes="value rootClass"/>
|
||||||
<tal:comment replace="nothing">Searches for this content type</tal:comment>
|
<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>
|
||||||
|
</div>
|
||||||
|
</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"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue