Basic refactoring of standard search functionality.
Move everything to loops.expert, making loops.search obsolete.
This commit is contained in:
parent
12de2376ac
commit
ad8c236aa5
12 changed files with 819 additions and 14 deletions
|
@ -108,9 +108,9 @@ class NodeView(BaseView):
|
|||
subMacros=[node_macros.macros['page_actions'],
|
||||
i18n_macros.macros['language_switch']])
|
||||
if self.globalOptions('expert.quicksearch'):
|
||||
from loops.expert.browser.search import searchMacrosTemplate
|
||||
from loops.expert.browser.search import search_template
|
||||
cm.register('top_actions', 'top_quicksearch', name='multi_actions',
|
||||
subMacros=[searchMacrosTemplate.macros['quicksearch']],
|
||||
subMacros=[search_template.macros['quicksearch']],
|
||||
priority=20)
|
||||
cm.register('portlet_left', 'navigation', title='Navigation',
|
||||
subMacro=node_macros.macros['menu'])
|
||||
|
|
|
@ -474,7 +474,7 @@
|
|||
<include package=".media" />
|
||||
<include package=".organize" />
|
||||
<include package=".rest" />
|
||||
<include package=".search" />
|
||||
<!--<include package=".search" />-->
|
||||
<include package=".security" />
|
||||
<include package=".system" />
|
||||
<include package=".versioning" />
|
||||
|
|
|
@ -16,7 +16,31 @@
|
|||
<browser:page
|
||||
name="search.html"
|
||||
for="loops.interfaces.INode"
|
||||
class="loops.expert.browser.search.SearchResults"
|
||||
class="loops.expert.browser.search.QuickSearchResults"
|
||||
permission="zope.View" />
|
||||
|
||||
<zope:adapter
|
||||
name="search"
|
||||
for="loops.interfaces.IConcept
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
provides="zope.interface.Interface"
|
||||
factory="loops.expert.browser.search.Search"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
<browser:page
|
||||
name="listConceptsForComboBox.js"
|
||||
for="loops.interfaces.ILoopsObject"
|
||||
class="loops.expert.browser.search.Search"
|
||||
attribute="listConcepts"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
<browser:page
|
||||
name="searchresults.html"
|
||||
for="loops.interfaces.ILoopsObject"
|
||||
class="loops.expert.browser.search.SearchResults"
|
||||
permission="zope.View"
|
||||
/>
|
||||
|
||||
</configure>
|
||||
|
|
|
@ -20,6 +20,57 @@
|
|||
</div>
|
||||
|
||||
|
||||
<metal:search define-macro="search"
|
||||
i18n:domain="loops">
|
||||
<div id="search"
|
||||
tal:define="macros item/search_macros;
|
||||
idPrefix string:${view/itemNum}.search;
|
||||
formId string:$idPrefix.form;
|
||||
resultsId string:$idPrefix.results;
|
||||
submitted request/search.submitted|nothing">
|
||||
<h1 tal:attributes="class string:content-$level;
|
||||
ondblclick item/openEditWindow"
|
||||
tal:content="item/title">
|
||||
Search
|
||||
</h1>
|
||||
|
||||
<div metal:define-macro="search_form" class="searchForm">
|
||||
<fieldset class="box">
|
||||
<form action="." method="post" id="1.search.form"
|
||||
tal:attributes="id formId">
|
||||
<input type="hidden" name="search.submitted" value="yes" />
|
||||
<input type="hidden" name="search.resultsId" value="1.results"
|
||||
tal:attributes="value resultsId" />
|
||||
<table cellpadding="3">
|
||||
<tal:block repeat="search_param python:
|
||||
['type', 'text', 'concept', 'state']">
|
||||
<metal:row use-macro="macros/search_row" />
|
||||
</tal:block>
|
||||
<tr tal:condition="nothing">
|
||||
<td colspan="2">
|
||||
<input type="button" value="Add concept filter" class="button" />
|
||||
<input type="button" value="Add attribute filter" class="button" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td colspan="3"><br />
|
||||
<input type="submit" name="button.search" value="Search" class="submit"
|
||||
i18n:attributes="value"
|
||||
tal:attributes="xx_onclick python:
|
||||
item.submitReplacing(resultsId, formId, view)" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<tal:results condition="request/search.submitted|nothing">
|
||||
<metal:results use-macro="item/search_macros/results" />
|
||||
</tal:results>
|
||||
</div>
|
||||
</metal:search>
|
||||
|
||||
<div metal:define-macro="search_results" id="search.results"
|
||||
tal:define="item nocall:view">
|
||||
<fieldset class="box">
|
||||
|
@ -87,5 +138,240 @@
|
|||
</div>
|
||||
|
||||
|
||||
<div metal:define-macro="search_row" id="1.1.row"
|
||||
tal:define="rowNum item/rowNum;
|
||||
basicIdPrefix idPrefix;
|
||||
idPrefix string:$idPrefix.$rowNum;
|
||||
namePrefix string:search.$rowNum;
|
||||
param search_param | item/searchParam"
|
||||
tal:attributes="id string:$idPrefix.row">
|
||||
<div metal:use-macro="macros/?param" />
|
||||
</div>
|
||||
|
||||
|
||||
<metal:text define-macro="type">
|
||||
<tr>
|
||||
<td metal:use-macro="macros/minus"/>
|
||||
<td colspan="3">
|
||||
<h2 i18n:translate="">Type(s) to search for</h2>
|
||||
</td>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<label for="text"
|
||||
tal:attributes="for string:$idPrefix.text">
|
||||
<span i18n:translate="">Type</span>:</label>
|
||||
<select name="text"
|
||||
tal:define="name string:$namePrefix.text;
|
||||
value request/?name|nothing"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.text;">
|
||||
<tal:types repeat="type item/typesForSearch">
|
||||
<option value="loops:*"
|
||||
i18n:translate=""
|
||||
tal:attributes="value type/token;
|
||||
selected python: value == type.token"
|
||||
tal:content="type/title">Topic</option>
|
||||
</tal:types>
|
||||
</select>
|
||||
<input type="button" value="+"
|
||||
title="Add type"
|
||||
tal:condition="nothing" />
|
||||
</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
</metal:text>
|
||||
|
||||
|
||||
<metal:text define-macro="text">
|
||||
<tr>
|
||||
<td metal:use-macro="macros/minus"/>
|
||||
<td colspan="3">
|
||||
<h2 i18n:translate="">Text-based search</h2>
|
||||
</td>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<input type="checkbox" value="yes"
|
||||
tal:define="name string:$namePrefix.title"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.title;
|
||||
checked request/?name|not:submitted|string:yes" />
|
||||
<label for="title"
|
||||
i18n:translate=""
|
||||
tal:attributes="for string:$idPrefix.title">Title</label>
|
||||
<input type="checkbox" value="yes"
|
||||
tal:define="name string:$namePrefix.full"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.full;
|
||||
checked request/?name|nothing" />
|
||||
<label for="full"
|
||||
i18n:translate=""
|
||||
tal:attributes="for string:$idPrefix.full">Full text</label>
|
||||
</td>
|
||||
<td>
|
||||
<label for="text"
|
||||
tal:attributes="for string:$idPrefix.text">
|
||||
<span i18n:translate="">Search text</span>:</label>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text"
|
||||
tal:define="name string:$namePrefix.text"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.text;
|
||||
value request/?name|nothing" />
|
||||
<input type="button" value="+"
|
||||
title="Add search word"
|
||||
tal:condition="nothing" />
|
||||
</td>
|
||||
</tr>
|
||||
</metal:text>
|
||||
|
||||
|
||||
<metal:text define-macro="concept">
|
||||
<tr>
|
||||
<td metal:use-macro="macros/minus"/>
|
||||
<td colspan="3">
|
||||
<h2 i18n:translate="">Search via related concepts</h2>
|
||||
</td>
|
||||
<tr tal:repeat="type item/presetSearchTypes">
|
||||
<tal:preset define="rowNum item/rowNum;
|
||||
idPrefix string:$basicIdPrefix.$rowNum;
|
||||
namePrefix string:search.$rowNum;">
|
||||
<td></td>
|
||||
<td>
|
||||
<span i18n:translate="">Type</span>:
|
||||
<b tal:content="type/title" i18n:translate="" />
|
||||
<input type="hidden" name="type" value=""
|
||||
tal:attributes="name string:$namePrefix.type;
|
||||
value type/token" />
|
||||
</td>
|
||||
<td>
|
||||
<label for="text"
|
||||
tal:attributes="for string:$idPrefix.text">
|
||||
<span i18n:translate="">Concept for Search</span>:</label>
|
||||
</td>
|
||||
<td>
|
||||
<select name="text_selected"
|
||||
tal:define="name string:$namePrefix.text_selected;
|
||||
value request/?name|nothing"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.text;">
|
||||
<tal:concepts repeat="concept python: item.conceptsForType(type['token'])">
|
||||
<option tal:attributes="value concept/token;
|
||||
selected python: value == concept['token']"
|
||||
i18n:translate=""
|
||||
tal:content="concept/title">Zope Corp</option>
|
||||
</tal:concepts>
|
||||
</select>
|
||||
</td>
|
||||
</tal:preset>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<label for="type"
|
||||
tal:attributes="for string:$idPrefix.type">
|
||||
<span i18n:translate="">Type</span>:</label>
|
||||
<select name="type"
|
||||
tal:define="name string:$namePrefix.type;
|
||||
global conceptTypeValue request/?name|string:"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.type;
|
||||
value conceptTypeValue;
|
||||
onChange string:setConceptTypeForComboBox('$idPrefix.type', '$idPrefix.text')">
|
||||
<tal:types repeat="type item/conceptTypesForSearch">
|
||||
<option value="loops:*"
|
||||
i18n:translate=""
|
||||
tal:attributes="value type/token;
|
||||
selected python: conceptTypeValue == type.token"
|
||||
tal:content="type/title">Topic</option>
|
||||
</tal:types>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<label for="text"
|
||||
tal:attributes="for string:$idPrefix.text">
|
||||
<span i18n:translate="">Concept for Search</span>:</label>
|
||||
<input type="text" name="text"
|
||||
tal:condition="nothing"
|
||||
tal:attributes="name string:$namePrefix.text;
|
||||
id string:$idPrefix.text;" />
|
||||
<input type="button" value="+"
|
||||
title="Add type"
|
||||
tal:condition="nothing" />
|
||||
</td>
|
||||
<td>
|
||||
<tal:combo tal:define="dummy item/initDojo;
|
||||
name string:$namePrefix.text;
|
||||
value request/?name|nothing;
|
||||
concept python:
|
||||
value and view.getObjectForUid(value);
|
||||
displayValue python:
|
||||
concept and concept.title or u''">
|
||||
<div dojoType="dojox.data.QueryReadStore" jsId="conceptSearch"
|
||||
url="listConceptsForComboBox.js?searchType="
|
||||
tal:attributes="url
|
||||
string:listConceptsForComboBox.js?searchType=$conceptTypeValue">
|
||||
</div>
|
||||
<input dojoType="dijit.form.FilteringSelect" store="conceptSearch"
|
||||
autoComplete="False" labelAttr="name" style="height: 16px"
|
||||
name="concept.search.text" id="concept.search.text"
|
||||
tal:attributes="name name;
|
||||
id string:$idPrefix.text;
|
||||
displayedValue displayValue" />
|
||||
</tal:combo>
|
||||
</td>
|
||||
</tr>
|
||||
</metal:text>
|
||||
|
||||
|
||||
<metal:text define-macro="state"
|
||||
tal:define="stateDefs item/statesDefinitions;
|
||||
deftype string:resource"
|
||||
tal:condition="stateDefs">
|
||||
<tr>
|
||||
<td metal:use-macro="macros/minus"/>
|
||||
<td colspan="3">
|
||||
<h2 i18n:translate="">Restrict to objects with certain states</h2>
|
||||
</td>
|
||||
<tr>
|
||||
<td></td>
|
||||
<th i18n:translate="">Workflow</th>
|
||||
<th colspan="2"
|
||||
i18n:translate="">States</th>
|
||||
</tr>
|
||||
<tr tal:repeat="def stateDefs">
|
||||
<td></td>
|
||||
<td valign="top"
|
||||
tal:content="def/name"
|
||||
i18n:translate=""></td>
|
||||
<td colspan="2">
|
||||
<tal:states repeat="state def/states">
|
||||
<tal:state define="name string:state.$deftype.${def/name};
|
||||
value state/name">
|
||||
<input type="checkbox"
|
||||
tal:attributes="name string:$name:list;
|
||||
value value;
|
||||
checked python:
|
||||
value in item.selectedStates.get(name, ());
|
||||
id string:$name.$value"
|
||||
/> <label tal:content="state/title"
|
||||
i18n:translate=""
|
||||
tal:attributes="for string:$name.$value" />
|
||||
|
||||
</tal:state>
|
||||
</tal:states>
|
||||
</td>
|
||||
</tr>
|
||||
</metal:text>
|
||||
|
||||
|
||||
<td metal:define-macro="minus">
|
||||
<input type="button" value="−"
|
||||
title="Remove search parameter"
|
||||
tal:condition="python:
|
||||
param not in ['type', 'text', 'concept', 'state']" />
|
||||
</td>
|
||||
|
||||
</html>
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2009 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2011 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -28,22 +28,26 @@ from zope.app.pagetemplate import ViewPageTemplateFile
|
|||
from zope.cachedescriptors.property import Lazy
|
||||
from zope.traversing.api import getName, getParent
|
||||
|
||||
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
|
||||
from loops.browser.common import BaseView
|
||||
from loops.browser.node import NodeView
|
||||
from loops.common import adapted, AdapterBase
|
||||
from loops.expert.concept import ConceptQuery, FullQuery
|
||||
from loops.interfaces import IResource
|
||||
from loops.organize.personal.browser.filter import FilterView
|
||||
from loops import util
|
||||
from loops.util import _
|
||||
|
||||
|
||||
searchMacrosTemplate = ViewPageTemplateFile('search.pt')
|
||||
search_template = ViewPageTemplateFile('search.pt')
|
||||
|
||||
|
||||
class SearchResults(NodeView):
|
||||
class QuickSearchResults(NodeView):
|
||||
""" Provides results listing """
|
||||
|
||||
@Lazy
|
||||
def search_macros(self):
|
||||
return self.controller.getTemplateMacros('search', searchMacrosTemplate)
|
||||
return self.controller.getTemplateMacros('search', search_template)
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
|
@ -64,3 +68,250 @@ class SearchResults(NodeView):
|
|||
result = fv.apply(result)
|
||||
result.sort(key=lambda x: x.title)
|
||||
return self.viewIterator(result)
|
||||
|
||||
|
||||
class Search(BaseView):
|
||||
|
||||
maxRowNum = 0
|
||||
|
||||
@Lazy
|
||||
def search_macros(self):
|
||||
return self.controller.getTemplateMacros('search', search_template)
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.search_macros['search']
|
||||
|
||||
@property
|
||||
def rowNum(self):
|
||||
""" Return the rowNum to be used for identifying the current search
|
||||
parameter row.
|
||||
"""
|
||||
n = self.request.get('loops.rowNum', 0)
|
||||
if n: # if given directly we don't use the calculation
|
||||
return n
|
||||
n = (self.maxRowNum or self.request.get('loops.maxRowNum', 0)) + 1
|
||||
self.maxRowNum = n
|
||||
return n
|
||||
|
||||
@Lazy
|
||||
def presetSearchTypes(self):
|
||||
""" Return a list of concept type info dictionaries (see BaseView)
|
||||
that should be displayed on a separate search parameter row.
|
||||
"""
|
||||
#return ITypeManager(self.context).listTypes(include=('search',))
|
||||
return self.listTypesForSearch(include=('search',))
|
||||
|
||||
def conceptsForType(self, token):
|
||||
result = ConceptQuery(self).query(type=token)
|
||||
fv = FilterView(self.context, self.request)
|
||||
result = fv.apply(result)
|
||||
result.sort(key=lambda x: x.title)
|
||||
noSelection = dict(token='none', title=u'not selected')
|
||||
return [noSelection] + [dict(title=adapted(o, self.languageInfo).title,
|
||||
token=util.getUidForObject(o))
|
||||
for o in result]
|
||||
|
||||
def initDojo(self):
|
||||
self.registerDojo()
|
||||
cm = self.controller.macros
|
||||
jsCall = ('dojo.require("dojo.parser");'
|
||||
'dojo.require("dijit.form.FilteringSelect");'
|
||||
'dojo.require("dojox.data.QueryReadStore");')
|
||||
cm.register('js-execute', jsCall, jsCall=jsCall)
|
||||
|
||||
def listConcepts(self, filterMethod=None):
|
||||
""" Used for dijit.FilteringSelect.
|
||||
"""
|
||||
request = self.request
|
||||
request.response.setHeader('Content-Type', 'text/plain; charset=UTF-8')
|
||||
title = request.get('name')
|
||||
if title == '*':
|
||||
title = None
|
||||
types = request.get('searchType')
|
||||
data = []
|
||||
if title or types:
|
||||
if title is not None:
|
||||
title = title.replace('(', ' ').replace(')', ' ').replace(' -', ' ')
|
||||
#title = title.split(' ', 1)[0]
|
||||
if not types:
|
||||
types = ['loops:concept:*']
|
||||
if not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
for type in types:
|
||||
result = self.executeQuery(title=title or None, type=type,
|
||||
exclude=('hidden',))
|
||||
fv = FilterView(self.context, self.request)
|
||||
result = fv.apply(result)
|
||||
for o in result:
|
||||
if o.getLoopsRoot() == self.loopsRoot:
|
||||
adObj = adapted(o, self.languageInfo)
|
||||
if filterMethod is not None and not filterMethod(adObj):
|
||||
continue
|
||||
name = self.getRowName(adObj)
|
||||
if title and title.endswith('*'):
|
||||
title = title[:-1]
|
||||
sort = ((title and name.startswith(title) and '0' or '1')
|
||||
+ name.lower())
|
||||
if o.conceptType is None:
|
||||
raise ValueError('Concept Type missing for %r.' % name)
|
||||
data.append({'label': self.getRowLabel(adObj, name),
|
||||
'name': name,
|
||||
'id': util.getUidForObject(o),
|
||||
'sort': sort})
|
||||
data.sort(key=lambda x: x['sort'])
|
||||
if not title:
|
||||
data.insert(0, {'label': '', 'name': '', 'id': ''})
|
||||
json = []
|
||||
for item in data[:100]:
|
||||
json.append("{label: '%s', name: '%s', id: '%s'}" %
|
||||
(item['label'], item['name'], item['id']))
|
||||
json = "{identifier: 'id', items: [%s]}" % ', '.join(json)
|
||||
#print '***', json
|
||||
return json
|
||||
|
||||
def executeQuery(self, **kw):
|
||||
return ConceptQuery(self).query(**kw)
|
||||
|
||||
def getRowName(self, obj):
|
||||
return obj.getLongTitle()
|
||||
|
||||
def getRowLabel(self, obj, name=None):
|
||||
if isinstance(obj, AdapterBase):
|
||||
obj = obj.context
|
||||
if name is None:
|
||||
name = obj.title
|
||||
return '%s (%s)' % (name, obj.conceptType.title)
|
||||
|
||||
@Lazy
|
||||
def statesDefinitions(self):
|
||||
return [component.getUtility(IStatesDefinition, name=n)
|
||||
for n in self.globalOptions('organize.stateful.resource', ())]
|
||||
|
||||
@Lazy
|
||||
def selectedStates(self):
|
||||
result = {}
|
||||
for k, v in self.request.form.items():
|
||||
if k.startswith('state.') and v:
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
def submitReplacing(self, targetId, formId, view):
|
||||
self.registerDojo()
|
||||
return 'submitReplacing("%s", "%s", "%s"); return false;' % (
|
||||
targetId, formId,
|
||||
'%s/.target%s/@@searchresults.html' % (view.url, self.uniqueId))
|
||||
|
||||
@Lazy
|
||||
def results(self):
|
||||
form = self.request.form
|
||||
type = form.get('search.1.text', 'loops:*')
|
||||
text = form.get('search.2.text')
|
||||
if text is not None:
|
||||
text = util.toUnicode(text, encoding='ISO8859-15') # IE hack!!!
|
||||
useTitle = form.get('search.2.title')
|
||||
useFull = form.get('search.2.full')
|
||||
conceptType = form.get('search.3.type', 'loops:concept:*')
|
||||
#conceptTitle = form.get('search.3.text')
|
||||
#if conceptTitle is not None:
|
||||
# conceptTitle = util.toUnicode(conceptTitle, encoding='ISO8859-15')
|
||||
conceptUid = form.get('search.3.text')
|
||||
result = FullQuery(self).query(text=text, type=type,
|
||||
useTitle=useTitle, useFull=useFull,
|
||||
#conceptTitle=conceptTitle,
|
||||
conceptUid=conceptUid,
|
||||
conceptType=conceptType)
|
||||
rowNum = 4
|
||||
while rowNum < 10:
|
||||
addCriteria = form.get('search.%i.text_selected' % rowNum)
|
||||
rowNum += 1
|
||||
if not addCriteria:
|
||||
break
|
||||
if addCriteria == 'none':
|
||||
continue
|
||||
addSelection = FullQuery(self).query(text=text, type=type,
|
||||
useTitle=useTitle, useFull=useFull,
|
||||
conceptUid=addCriteria)
|
||||
if result:
|
||||
result = [r for r in result if r in addSelection]
|
||||
else:
|
||||
result = addSelection
|
||||
result = [r for r in result if self.checkStates(r)]
|
||||
fv = FilterView(self.context, self.request)
|
||||
result = fv.apply(result)
|
||||
result = sorted(result, key=lambda x: x.title.lower())
|
||||
return self.viewIterator(result)
|
||||
|
||||
def checkStates(self, obj):
|
||||
if not IResource.providedBy(obj):
|
||||
return True
|
||||
for std, states in self.selectedStates.items():
|
||||
if std.startswith('state.resource.'):
|
||||
std = std[len('state.resource.'):]
|
||||
else:
|
||||
continue
|
||||
stf = component.queryAdapter(obj, IStateful, name=std)
|
||||
if stf is None:
|
||||
continue
|
||||
for state in states:
|
||||
if stf.state == state:
|
||||
break
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
#class SearchResults(BaseView):
|
||||
class SearchResults(NodeView):
|
||||
""" Provides results as inner HTML """
|
||||
|
||||
@Lazy
|
||||
def search_macros(self):
|
||||
return self.controller.getTemplateMacros('search', search_template)
|
||||
|
||||
@Lazy
|
||||
def macro(self):
|
||||
return self.search_macros['search_results']
|
||||
|
||||
def __call__(self):
|
||||
return innerHtml(self)
|
||||
|
||||
@Lazy
|
||||
def results(self):
|
||||
form = self.request.form
|
||||
type = form.get('search.1.text', 'loops:*')
|
||||
text = form.get('search.2.text')
|
||||
if text is not None:
|
||||
text = util.toUnicode(text, encoding='ISO8859-15') # IE hack!!!
|
||||
useTitle = form.get('search.2.title')
|
||||
useFull = form.get('search.2.full')
|
||||
conceptType = form.get('search.3.type', 'loops:concept:*')
|
||||
#conceptTitle = form.get('search.3.text')
|
||||
#if conceptTitle is not None:
|
||||
# conceptTitle = util.toUnicode(conceptTitle, encoding='ISO8859-15')
|
||||
conceptUid = form.get('search.3.text')
|
||||
result = FullQuery(self).query(text=text, type=type,
|
||||
useTitle=useTitle, useFull=useFull,
|
||||
#conceptTitle=conceptTitle,
|
||||
conceptUid=conceptUid,
|
||||
conceptType=conceptType)
|
||||
rowNum = 4
|
||||
while rowNum < 10:
|
||||
addCriteria = form.get('search.%i.text_selected' % rowNum)
|
||||
rowNum += 1
|
||||
if not addCriteria:
|
||||
break
|
||||
if addCriteria == 'none':
|
||||
continue
|
||||
addSelection = FullQuery(self).query(text=text, type=type,
|
||||
useTitle=useTitle, useFull=useFull,
|
||||
conceptUid=addCriteria)
|
||||
if result:
|
||||
result = [r for r in result if r in addSelection]
|
||||
else:
|
||||
result = addSelection
|
||||
fv = FilterView(self.context, self.request)
|
||||
result = fv.apply(result)
|
||||
result = sorted(result, key=lambda x: x.title.lower())
|
||||
return self.viewIterator(result)
|
||||
|
||||
|
|
239
expert/search.txt
Executable file
239
expert/search.txt
Executable file
|
@ -0,0 +1,239 @@
|
|||
===================================================================
|
||||
loops.search - Provide search functionality for the loops framework
|
||||
===================================================================
|
||||
|
||||
($Id$)
|
||||
|
||||
|
||||
Let's do some basic set up
|
||||
|
||||
>>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
|
||||
>>> site = placefulSetUp(True)
|
||||
|
||||
>>> from zope import component, interface
|
||||
>>> from zope.interface import implements
|
||||
|
||||
and setup a simple loops site with a concept manager and some concepts
|
||||
(with all the type machinery, what in real life is done via standard
|
||||
ZCML setup):
|
||||
|
||||
>>> from loops.concept import Concept
|
||||
>>> from loops.type import ConceptType, TypeConcept
|
||||
>>> from loops.interfaces import ITypeConcept
|
||||
>>> from loops.base import Loops
|
||||
>>> from loops.expert.testsetup import TestSite
|
||||
>>> t = TestSite(site)
|
||||
>>> concepts, resources, views = t.setup()
|
||||
|
||||
>>> loopsRoot = site['loops']
|
||||
>>> query = concepts['query']
|
||||
>>> topic = concepts['topic']
|
||||
|
||||
In addition we create a concept that holds the search page and a node
|
||||
(page) that links to this concept:
|
||||
|
||||
>>> search = concepts['search'] = Concept(u'Search')
|
||||
>>> search.conceptType = query
|
||||
|
||||
>>> from loops.view import Node
|
||||
>>> page = views['page'] = Node('Search Page')
|
||||
>>> page.target = search
|
||||
|
||||
Search views
|
||||
------------
|
||||
|
||||
Now we are ready to create a search view object:
|
||||
|
||||
>>> from zope.publisher.browser import TestRequest
|
||||
>>> from loops.expert.browser.search import Search
|
||||
>>> searchView = Search(search, TestRequest())
|
||||
|
||||
The search view provides values for identifying the search form itself
|
||||
and the parameter rows; the rowNum is auto-incremented, so it should be
|
||||
accessed exactly once per row:
|
||||
|
||||
>>> searchView.rowNum
|
||||
1
|
||||
>>> searchView.rowNum
|
||||
2
|
||||
|
||||
The search view provides vocabularies for types that allow the selection
|
||||
of types to search for; this needs an ITypeManager adapter registered via
|
||||
zcml in real life:
|
||||
|
||||
>>> from loops.type import LoopsTypeManager
|
||||
>>> component.provideAdapter(LoopsTypeManager)
|
||||
|
||||
>>> t = searchView.typesForSearch()
|
||||
>>> len(t)
|
||||
14
|
||||
>>> t.getTermByToken('loops:resource:*').title
|
||||
'Any Resource'
|
||||
|
||||
>>> t = searchView.conceptTypesForSearch()
|
||||
>>> len(t)
|
||||
11
|
||||
>>> t.getTermByToken('loops:concept:*').title
|
||||
'Any Concept'
|
||||
|
||||
To execute the search in the context of a node we have to set up a node
|
||||
view for our page. The submitReplacing method returns a JavaScript call
|
||||
that will replace the results part on the search page; as this registers
|
||||
the dojo library with the view's controller we also have to supply
|
||||
a controller attribute for the search view.
|
||||
|
||||
>>> from loops.browser.node import NodeView
|
||||
>>> request = TestRequest()
|
||||
>>> pageView = NodeView(page, request)
|
||||
|
||||
>>> from cybertools.browser.liquid.controller import Controller
|
||||
>>> searchView.controller = Controller(searchView, request)
|
||||
|
||||
>>> searchView.submitReplacing('1.results', '1.search.form', pageView)
|
||||
'submitReplacing("1.results", "1.search.form",
|
||||
"http://127.0.0.1/loops/views/page/.target80/@@searchresults.html");...'
|
||||
|
||||
Basic (text/title) search
|
||||
-------------------------
|
||||
|
||||
The searchresults.html view, i.e. the SearchResults view class provides the
|
||||
result set of the search via its `results` property.
|
||||
|
||||
Before accessing the `results` property we have to prepare a
|
||||
resource we can search for and index it in the catalog.
|
||||
|
||||
>>> from loops.resource import Resource
|
||||
>>> rplone = resources['plone'] = Resource()
|
||||
|
||||
>>> from zope.app.catalog.interfaces import ICatalog
|
||||
>>> from loops import util
|
||||
>>> catalog = component.getUtility(ICatalog)
|
||||
>>> catalog.index_doc(int(util.getUidForObject(rplone)), rplone)
|
||||
|
||||
>>> from loops.expert.browser.search import SearchResults
|
||||
>>> form = {'search.2.title': True, 'search.2.text': u'plone'}
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> resultsView = SearchResults(page, request)
|
||||
|
||||
>>> results = list(resultsView.results)
|
||||
>>> len(results)
|
||||
1
|
||||
>>> results[0].context == rplone
|
||||
True
|
||||
|
||||
>>> form = {'search.2.title': True, 'search.2.text': u'foo'}
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> resultsView = SearchResults(page, request)
|
||||
>>> len(list(resultsView.results))
|
||||
0
|
||||
|
||||
Search via related concepts
|
||||
---------------------------
|
||||
|
||||
We first have to prepare some test concepts (topics); we also assign our test
|
||||
resource (rplone) from above to one of the topics:
|
||||
|
||||
>>> czope = concepts['zope'] = Concept(u'Zope')
|
||||
>>> czope2 = concepts['zope2'] = Concept(u'Zope 2')
|
||||
>>> czope3 = concepts['zope3'] = Concept(u'Zope 3')
|
||||
>>> cplone = concepts['plone'] = Concept(u'Plone')
|
||||
>>> for c in (czope, czope2, czope3, cplone):
|
||||
... c.conceptType = topic
|
||||
... catalog.index_doc(int(util.getUidForObject(c)), c)
|
||||
>>> czope.assignChild(czope2)
|
||||
>>> czope.assignChild(czope3)
|
||||
>>> czope2.assignChild(cplone)
|
||||
>>> rplone.assignConcept(cplone)
|
||||
|
||||
Now we can fill our search form and execute the query; note that all concepts
|
||||
found are listed, plus all their children and all resources associated
|
||||
with them:
|
||||
|
||||
>>> uid = util.getUidForObject(concepts['zope'])
|
||||
>>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': uid}
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> resultsView = SearchResults(page, request)
|
||||
>>> results = list(resultsView.results)
|
||||
>>> len(results)
|
||||
5
|
||||
>>> results[0].context.__name__
|
||||
u'plone'
|
||||
|
||||
>>> uid = util.getUidForObject(concepts['zope3'])
|
||||
>>> form = {'search.3.type': 'loops:concept:topic', 'search.3.text': uid}
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> resultsView = SearchResults(page, request)
|
||||
>>> results = list(resultsView.results)
|
||||
>>> len(results)
|
||||
1
|
||||
>>> results[0].context.__name__
|
||||
u'zope3'
|
||||
|
||||
To support easy entry of concepts to search for we can preselect the available
|
||||
concepts (optionally restricted to a certain type) by entering text parts
|
||||
of the concepts' titles:
|
||||
|
||||
>>> form = {'searchType': 'loops:concept:topic', 'name': u'zope'}
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> view = Search(page, request)
|
||||
>>> view.listConcepts()
|
||||
u"{identifier: 'id', items: [{label: 'Zope (Topic)', name: 'Zope', id: '85'}, {label: 'Zope 2 (Topic)', name: 'Zope 2', id: '87'}, {label: 'Zope 3 (Topic)', name: 'Zope 3', id: '89'}]}"
|
||||
|
||||
Preset Concept Types on Search Forms
|
||||
------------------------------------
|
||||
|
||||
Often we want to include certain types in our search. We can instruct
|
||||
the search form to include lines for these types by giving these types
|
||||
a certain qualifier, via the option attribute of the type interface.
|
||||
|
||||
Let's start with a new type, the customer type.
|
||||
|
||||
>>> customer = concepts['customer']
|
||||
>>> custType = ITypeConcept(customer)
|
||||
>>> custType.options
|
||||
[]
|
||||
|
||||
>>> cust1 = concepts['cust1']
|
||||
>>> cust2 = concepts['cust2']
|
||||
>>> for c in (cust1, cust2):
|
||||
... c.conceptType = customer
|
||||
... catalog.index_doc(int(util.getUidForObject(c)), c)
|
||||
|
||||
>>> from cybertools.typology.interfaces import IType
|
||||
>>> IType(cust1).qualifiers
|
||||
('concept',)
|
||||
|
||||
>>> searchView = Search(search, TestRequest())
|
||||
>>> list(searchView.presetSearchTypes)
|
||||
[]
|
||||
|
||||
We can now add a 'search' qualifier to the customer type's options
|
||||
and thus include the customer type in the preset search types.
|
||||
|
||||
>>> custType.options = ('qualifier:search',)
|
||||
>>> IType(cust1).qualifiers
|
||||
('concept', 'search')
|
||||
>>> searchView = Search(search, TestRequest())
|
||||
>>> list(searchView.presetSearchTypes)
|
||||
[{'token': 'loops:concept:customer', 'title': u'Customer'}]
|
||||
|
||||
>>> searchView.conceptsForType('loops:concept:customer')
|
||||
[{'token': 'none', 'title': u'not selected'},
|
||||
{'token': '58', 'title': u'Customer 1'},
|
||||
{'token': '60', 'title': u'Customer 2'},
|
||||
{'token': '62', 'title': u'Customer 3'}]
|
||||
|
||||
Let's use this new search option for querying:
|
||||
|
||||
>>> form = {'search.4.text_selected': u'58'}
|
||||
>>> resultsView = SearchResults(page, TestRequest(form=form))
|
||||
>>> results = list(resultsView.results)
|
||||
>>> results[0].title
|
||||
u'Customer 1'
|
||||
|
||||
|
||||
Automatic Filtering
|
||||
-------------------
|
||||
|
||||
TODO - more to come...
|
||||
|
|
@ -24,6 +24,7 @@ def test_suite():
|
|||
return unittest.TestSuite((
|
||||
unittest.makeSuite(Test),
|
||||
DocFileSuite('README.txt', optionflags=flags),
|
||||
DocFileSuite('search.txt', optionflags=flags),
|
||||
))
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -31,7 +31,7 @@ from cybertools.stateful.interfaces import IStateful, IStatesDefinition
|
|||
from loops.browser.common import BaseView
|
||||
from loops.browser.concept import ConceptView
|
||||
from loops.expert.query import And, Or, State, Type, getObjects
|
||||
from loops.search.browser import search_template
|
||||
from loops.expert.browser.search import search_template
|
||||
from loops.util import _
|
||||
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ Search views
|
|||
Now we are ready to create a search view object:
|
||||
|
||||
>>> from zope.publisher.browser import TestRequest
|
||||
>>> from loops.search.browser import Search
|
||||
>>> from loops.expert.browser.search import Search
|
||||
>>> searchView = Search(search, TestRequest())
|
||||
|
||||
The search view provides values for identifying the search form itself
|
||||
|
@ -110,7 +110,7 @@ resource we can search for and index it in the catalog.
|
|||
>>> catalog = component.getUtility(ICatalog)
|
||||
>>> catalog.index_doc(int(util.getUidForObject(rplone)), rplone)
|
||||
|
||||
>>> from loops.search.browser import SearchResults
|
||||
>>> from loops.expert.browser.search import SearchResults
|
||||
>>> form = {'search.2.title': True, 'search.2.text': u'plone'}
|
||||
>>> request = TestRequest(form=form)
|
||||
>>> resultsView = SearchResults(page, request)
|
||||
|
|
|
@ -37,6 +37,7 @@ from cybertools.typology.interfaces import ITypeManager
|
|||
from loops.browser.common import BaseView
|
||||
from loops.browser.node import NodeView
|
||||
from loops.common import adapted, AdapterBase
|
||||
#from loops.expert.browser.search import searchMacrosTemplate as search_template
|
||||
from loops.expert.concept import ConceptQuery, FullQuery
|
||||
from loops.interfaces import IResource
|
||||
from loops.organize.personal.browser.filter import FilterView
|
||||
|
@ -44,7 +45,7 @@ from loops import util
|
|||
from loops.util import _
|
||||
|
||||
|
||||
search_template = ViewPageTemplateFile('search.pt')
|
||||
#search_template = ViewPageTemplateFile('search.pt')
|
||||
|
||||
|
||||
class Search(BaseView):
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
i18n_domain="zope"
|
||||
>
|
||||
|
||||
<zope:adapter
|
||||
<!--<zope:adapter
|
||||
name="search"
|
||||
for="loops.interfaces.IConcept
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
|
@ -28,6 +28,6 @@
|
|||
for="loops.interfaces.ILoopsObject"
|
||||
class="loops.search.browser.SearchResults"
|
||||
permission="zope.View"
|
||||
/>
|
||||
/>-->
|
||||
|
||||
</configure>
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<tal:results condition="request/search.submitted|nothing">
|
||||
<metal:results use-macro="item/search_macros/results" />
|
||||
</tal:results>
|
||||
|
||||
<div metal:define-macro="search_results" id="1.search.results"
|
||||
tal:attributes="id resultsId | request/search.resultsId"
|
||||
|
|
Loading…
Add table
Reference in a new issue