Skip to content

Commit

Permalink
Problem: user get unrelated results
Browse files Browse the repository at this point in the history
Solution: let user filter by fields in preview
  • Loading branch information
gotcha committed May 8, 2021
1 parent 464f4ea commit ba55e2b
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 10 deletions.
67 changes: 66 additions & 1 deletion collective/searchandreplace/browser/searchreplacetable.pt
@@ -1,3 +1,63 @@
<div
class="field"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
i18n:domain="SearchAndReplace"
tal:condition="python: request.has_key('form.actions.Preview') or request.has_key('form.actions.Replace')"
tal:define="fields view/getAffectedFields;
filter_fields view/filter_fields;"
>
<label onclick="toggleHidden('#fieldFilter');"
>
<span i18n:translate="">Filter by fields</span> (<span
tal:replace="python:len(fields)"
></span
>)
</label>

<div id="fieldFilter" class="hiddenStructure"
tal:attributes="class view/getFieldFilterClass">
<div>
<input
class="noborder"
src="select_all_icon.gif"
name="selectButton"
title="Select all items"
onclick="toggleSelect(this,'form.filterFields', true);"
alt="Select all items"
i18n:attributes="title;alt"
type="checkbox"
checked="checked"
/>
<span i18n:translate="">Select all items</span>
</div>

<div>
<tal:fields repeat="field fields">
<span>
<input
type="checkbox"
id="form.filterFields"
checked="checked"
name="form.filterFields"
tal:attributes="value field/value;
checked python:field.value in filter_fields"
/>
<span tal:replace="field/label" />
</span>
</tal:fields>
</div>

<input
type="submit"
id="form.actions.Preview"
name="form.actions.Preview"
class="button"
i18n:attributes="value"
value="Preview"
/>
</div>
</div>

<div
class="field"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
Expand Down Expand Up @@ -128,9 +188,14 @@
</div>

<script>
if (typeof toggleHidden !== "function") {
// toggleSelect is not included in Plone 5, we need to include it.
function toggleHidden(selector) {
jQuery(selector).toggleClass('hiddenStructure');
}
}
if (typeof toggleSelect !== "function") {
// toggleSelect is not included in Plone 5, we need to include it.

function toggleSelect(selectbutton, id, initialState, formName) {
/* required selectbutton: you can pass any object that will function as a toggle
* optional id: id of the the group of checkboxes that needs to be toggled (default=ids:list
Expand Down
50 changes: 48 additions & 2 deletions collective/searchandreplace/browser/searchreplacetable.py
Expand Up @@ -4,17 +4,29 @@
from zope.component import getUtility
from plone.registry.interfaces import IRegistry
from zope.publisher.browser import BrowserView
from collections import namedtuple
import six


Field = namedtuple('Field', ['label', 'value'])


class SearchReplaceTable(BrowserView):
""" View class for search and replace preview table widget."""

def __init__(self, context, request):
super(SearchReplaceTable, self).__init__(context, request)
self.results = self.findObjects()
self.computeAffectedFields()
self.filter_fields = self.getFilterFields()
self.are_fields_filtered = self.filter_fields != self.fields

def maximum_text_characters(self):
registry = getUtility(IRegistry)
settings = registry.forInterface(ISearchReplaceSettings, check=False)
return settings.maximum_text_characters

def getItems(self):
def findObjects(self):
""" Get preview items """
results = []
findWhat = self.request.get("form.findWhat", "")
Expand All @@ -38,10 +50,44 @@ def getItems(self):
)
return results

def computeAffectedFields(self):
self.fields = set()
self.field_labels = set()
for item in self.results:
self.fields.add(item['field'])
self.field_labels.add(Field(item['label'], item['field']))

def getAffectedFields(self):
results = list(self.field_labels)
results.sort()
return results

def getFieldFilterClass(self):
return '' if self.are_fields_filtered else 'hiddenStructure'

def getFilterFields(self):
fields = self.request.get("form.filterFields", None)
if fields is None:
# reset to all fields when no fields are selected
return self.fields
elif isinstance(fields, six.text_type) or isinstance(fields, six.binary_type):
# take care of a single field being selected
result = set()
result.add(fields)
return result
else:
return set(fields)

def getItems(self):
if not self.are_fields_filtered:
return self.results
else:
return [item for item in self.results if item['field'] in self.filter_fields]

def getRelativePath(self, path):
""" Get a relative path """
cpath = "/".join(self.context.getPhysicalPath())
rpath = path[len(cpath) :]
rpath = path[len(cpath):]
if rpath:
rpath = "." + rpath
else:
Expand Down
4 changes: 4 additions & 0 deletions collective/searchandreplace/locales/SearchAndReplace.pot
Expand Up @@ -45,6 +45,10 @@ msgstr ""
msgid "Fast search"
msgstr ""

#: ./browser/searchreplacetable.pt:11
msgid "Filter by fields"
msgstr ""

#: ./browser/searchreplaceform.py:27
msgid "Find What"
msgstr ""
Expand Down
Expand Up @@ -44,6 +44,10 @@ msgstr "Entrez le texte qui remplacera le texte original."
msgid "Fast search"
msgstr "Recherche rapide"

#: ./browser/searchreplacetable.pt:11
msgid "Filter by fields"
msgstr "Filtrer par champs"

#: ./browser/searchreplaceform.py:27
msgid "Find What"
msgstr "Rechercher"
Expand Down Expand Up @@ -154,7 +158,7 @@ msgstr "Rechercher/Remplacer"

#: ./browser/searchreplacetable.pt:20
msgid "Select all items"
msgstr "Tout selectionner"
msgstr "Tout sélectionner"

#: ./interfaces.py:62
msgid "The maximum number of characters to show before and after the found text."
Expand Down
9 changes: 6 additions & 3 deletions collective/searchandreplace/searchreplaceutility.py
Expand Up @@ -318,14 +318,15 @@ def find_matches_in_object(matcher, obj):
mobj = matcher.finditer(title)
for x in mobj:
start, end = x.span()
label = translate(PloneMessageFactory(u"Title"), context=obj.REQUEST)
results.append(
{
"path": path,
"url": obj.absolute_url(),
"field": "title",
"label": label,
"line": "title",
"linecol": translate(
PloneMessageFactory(u"Title"), context=obj.REQUEST
),
"linecol": label,
"pos": "%d" % start,
"text": getLinePreview(title, start, end),
}
Expand All @@ -347,6 +348,8 @@ def find_matches_in_object(matcher, obj):
{
"path": path,
"url": obj.absolute_url(),
"field": field.__name__,
"label": label,
"line": "%s %d" % (field.__name__, getLineNumber(text, start)),
"linecol": "%s %d" % (label, getLineNumber(text, start)),
"pos": "%d" % start,
Expand Down
6 changes: 3 additions & 3 deletions collective/searchandreplace/testing.py
Expand Up @@ -39,7 +39,7 @@ def setUpPloneSite(self, portal):

applyProfile(portal, "collective.searchandreplace:default")
setRoles(portal, TEST_USER_ID, ["Manager"])
create_doc(portal, text=u"Get Plone now")
create_doc(portal, text=u"Get Plone now", title=u"Plone", description=u"Plone")
setRoles(portal, TEST_USER_ID, ["Member"])


Expand All @@ -66,9 +66,9 @@ def rich_text(text):
)


def create_doc(container, id="page", title=u"Title of page", text=u""):
def create_doc(container, id="page", title=u"Title of page", text=u"", description=u""):
text = rich_text(text)
api.content.create(container, "Document", id=id, title=title, text=text)
api.content.create(container, "Document", id=id, title=title, text=text, description=description)


def edit_content(
Expand Down
66 changes: 66 additions & 0 deletions collective/searchandreplace/tests/filterfields.txt
@@ -0,0 +1,66 @@
==============================================================================
Filter by field test
==============================================================================

Create the browser object we'll be using.

>>> from plone.testing import z2
>>> from plone.app.testing import SITE_OWNER_NAME
>>> from plone.app.testing import SITE_OWNER_PASSWORD
>>> browser = z2.Browser(layer['app'])
>>> portal_url = layer['portal'].absolute_url()

Open the portal and login

>>> browser.open(portal_url + '/page')
>>> browser.getLink('Log in').click()
>>> browser.getControl('Login Name').value = SITE_OWNER_NAME
>>> browser.getControl('Password').value = SITE_OWNER_PASSWORD
>>> browser.getControl('Log in').click()

Test to ensure preview works. Search for 'Plone'
and check for three results, one per field.

>>> browser.open(portal_url)
>>> browser.getLink('Search/Replace').click()
>>> browser.getControl(name='form.findWhat').value = 'Plone'
>>> browser.getControl('Preview', index=0).click()
>>> browser.getControl(name='form.filterFields').options
['description', 'text', 'title']
>>> browser.getControl(name='form.filterFields').value
['description', 'text', 'title']
>>> browser.getControl(name='form.affectedContent').options
['title:0:/plone/page', 'description 1:0:/plone/page', 'text 1:4:/plone/page']

Show content only from title field.

>>> browser.getControl(name='form.filterFields').value = ['title']
>>> browser.getControl('Preview', index=0).click()
>>> browser.getControl(name='form.filterFields').options
['description', 'text', 'title']
>>> browser.getControl(name='form.filterFields').value
['title']
>>> browser.getControl(name='form.affectedContent').options
['title:0:/plone/page']

Show content from text and description fields.

>>> browser.getControl(name='form.filterFields').value = ['text', 'description']
>>> browser.getControl('Preview', index=0).click()
>>> browser.getControl(name='form.filterFields').options
['description', 'text', 'title']
>>> browser.getControl(name='form.filterFields').value
['description', 'text']
>>> browser.getControl(name='form.affectedContent').options
['description 1:0:/plone/page', 'text 1:4:/plone/page']

Unchecking all fields resets filter.

>>> browser.getControl(name='form.filterFields').value = []
>>> browser.getControl('Preview', index=0).click()
>>> browser.getControl(name='form.filterFields').options
['description', 'text', 'title']
>>> browser.getControl(name='form.filterFields').value
['description', 'text', 'title']
>>> browser.getControl(name='form.affectedContent').options
['title:0:/plone/page', 'description 1:0:/plone/page', 'text 1:4:/plone/page']
20 changes: 20 additions & 0 deletions collective/searchandreplace/tests/test_doctests.py
Expand Up @@ -29,11 +29,31 @@ def test_suite():
layer=SEARCH_REPLACE_FUNCTIONAL_LAYER,
)

filterfieldstest = layered(
doctest.DocFileSuite(
"tests/filterfields.txt",
package="collective.searchandreplace",
optionflags=oflags,
),
layer=SEARCH_REPLACE_FUNCTIONAL_LAYER,
)

suite.addTests(
[
basicsearchtest,
searchavailabletest,
]
)

try:
from zope.testbrowser.browser import webtest
suite.addTests(
[
filterfieldstest,
]
)

except ImportError:
pass

return suite

0 comments on commit ba55e2b

Please sign in to comment.