0002-search-search-on-page-and-subpages-40224.patch
combo/apps/search/forms.py | ||
---|---|---|
15 | 15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
16 | 16 | |
17 | 17 |
from django import forms |
18 |
from django.utils.translation import ugettext_lazy as _ |
|
18 | 19 | |
20 |
from combo.data.models import Page |
|
19 | 21 |
from .models import SearchCell |
20 | 22 | |
21 | 23 | |
... | ... | |
23 | 25 |
class Meta: |
24 | 26 |
model = SearchCell |
25 | 27 |
fields = ('autofocus', ) |
28 | ||
29 | ||
30 |
class SelectPageForm(forms.ModelForm): |
|
31 |
selected_page = forms.ModelChoiceField( |
|
32 |
label=_('Page'), |
|
33 |
required=False, |
|
34 |
queryset=Page.objects.none(), |
|
35 |
help_text=_("Select a page to limit the search on this page and sub pages contents."), |
|
36 |
) |
|
37 | ||
38 |
class Meta: |
|
39 |
model = SearchCell |
|
40 |
fields = [] |
|
41 | ||
42 |
def __init__(self, *args, **kwargs): |
|
43 |
super().__init__(*args, **kwargs) |
|
44 |
used_slugs = [ |
|
45 |
e['slug'].replace('_text_page_', '') for e in self.instance.search_services |
|
46 |
if e['slug'].startswith('_text_page_')] |
|
47 |
pages_queryset = ( |
|
48 |
Page.objects |
|
49 |
.filter(snapshot__isnull=True, sub_slug='') |
|
50 |
.exclude(slug__in=used_slugs) |
|
51 |
.order_by('title')) |
|
52 |
self.fields['selected_page'].queryset = pages_queryset |
|
53 |
# if '_text' without page is already selected, page is required |
|
54 |
if any(e['slug'] == '_text' for e in self.instance.search_services): |
|
55 |
self.fields['selected_page'].required = True |
combo/apps/search/manager_views.py | ||
---|---|---|
17 | 17 |
from django.http import HttpResponse |
18 | 18 |
from django.http import HttpResponseRedirect |
19 | 19 |
from django.shortcuts import get_object_or_404 |
20 |
from django.shortcuts import render |
|
20 | 21 |
from django.urls import reverse |
21 | 22 |
from django.utils.translation import ugettext_lazy as _ |
22 | 23 | |
24 |
from combo.apps.search.forms import SelectPageForm |
|
23 | 25 |
from combo.apps.search.models import SearchCell |
24 | 26 |
from combo.data.models import PageSnapshot |
25 | 27 | |
... | ... | |
27 | 29 |
def page_search_cell_add_engine(request, page_pk, cell_reference, engine_slug): |
28 | 30 |
cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk) |
29 | 31 | |
30 |
if engine_slug in cell.available_engines: |
|
31 |
if not cell._search_services or not cell._search_services.get('data'): |
|
32 |
cell._search_services = {'data': []} |
|
33 |
cell._search_services['data'].append(engine_slug) |
|
34 |
cell.save() |
|
35 |
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell) |
|
36 | ||
37 |
return HttpResponseRedirect('%s#cell-%s' % ( |
|
38 |
reverse('combo-manager-page-view', kwargs={'pk': page_pk}), |
|
39 |
cell_reference)) |
|
32 |
def add_slug(slug): |
|
33 |
if slug in cell.available_engines or slug.startswith('_text_page'): |
|
34 |
if not cell._search_services or not cell._search_services.get('data'): |
|
35 |
cell._search_services = {'data': []} |
|
36 |
cell._search_services['data'].append(slug) |
|
37 |
cell.save() |
|
38 |
PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell) |
|
39 |
return HttpResponseRedirect('%s#cell-%s' % ( |
|
40 |
reverse('combo-manager-page-view', kwargs={'pk': page_pk}), |
|
41 |
cell_reference)) |
|
42 | ||
43 |
if engine_slug != '_text': |
|
44 |
# add engine without intermediary form and popup |
|
45 |
return add_slug(engine_slug) |
|
46 | ||
47 |
if request.method == 'POST': |
|
48 |
form = SelectPageForm(instance=cell, data=request.POST) |
|
49 |
if form.is_valid(): |
|
50 |
slug = '_text' |
|
51 |
if form.cleaned_data['selected_page'] is not None: |
|
52 |
slug = '_text_page_%s' % form.cleaned_data['selected_page'].slug |
|
53 |
return add_slug(slug) |
|
54 |
else: |
|
55 |
form = SelectPageForm(instance=cell) |
|
56 |
context = { |
|
57 |
'form': form, |
|
58 |
'cell': cell, |
|
59 |
} |
|
60 |
return render(request, 'combo/manager/add-engine-form.html', context) |
|
40 | 61 | |
41 | 62 | |
42 | 63 |
def page_search_cell_delete_engine(request, page_pk, cell_reference, engine_slug): |
combo/apps/search/models.py | ||
---|---|---|
36 | 36 |
from . import engines |
37 | 37 | |
38 | 38 | |
39 |
def get_root_page_and_children(service_slug): |
|
40 |
if not service_slug.startswith('_text_page_'): |
|
41 |
return |
|
42 |
page_slug = service_slug.replace('_text_page_', '') |
|
43 |
try: |
|
44 |
root_page = Page.objects.get(slug=page_slug, sub_slug='') |
|
45 |
except (Page.DoesNotExist, Page.MultipleObjectsReturned): |
|
46 |
return |
|
47 |
return root_page.get_descendants_and_me() |
|
48 | ||
49 | ||
39 | 50 |
@register_cell_class |
40 | 51 |
class SearchCell(CellBase): |
41 | 52 |
template_name = 'combo/search-cell.html' |
... | ... | |
68 | 79 |
services = [] |
69 | 80 |
for service_slug in self._search_services.get('data') or []: |
70 | 81 |
service = engines.get(service_slug) |
82 |
if service_slug.startswith('_text_page_'): |
|
83 |
service = engines.get('_text') |
|
71 | 84 |
if service and (service.get('url') or service.get('function')): |
72 | 85 |
service['slug'] = service_slug |
73 | 86 |
services.append(service) |
... | ... | |
76 | 89 |
@cached_property |
77 | 90 |
def available_engines(self): |
78 | 91 |
all_engines = engines.get_engines() |
79 |
current_engines = [e['slug'] for e in self.search_services] |
|
92 |
# always remove _text engine: we can add search on page and sub pages |
|
93 |
current_engines = [e['slug'] for e in self.search_services if e['slug'] != '_text'] |
|
80 | 94 |
return {k: v for k, v in all_engines.items() if k not in current_engines} |
81 | 95 | |
96 |
def get_search_services_for_display(self): |
|
97 |
# get pages for _text engines |
|
98 |
page_slugs = [ |
|
99 |
e['slug'].replace('_text_page_', '') for e in self.search_services |
|
100 |
if e['slug'].startswith('_text_page_')] |
|
101 |
pages = ( |
|
102 |
Page.objects |
|
103 |
.filter(snapshot__isnull=True, sub_slug='', slug__in=page_slugs) |
|
104 |
.values('slug', 'title')) |
|
105 |
pages_by_slug = {'_text_page_%s' % p['slug']: p['title'] for p in pages} |
|
106 |
services = [] |
|
107 |
for service in self.search_services: |
|
108 |
label = service['label'] |
|
109 |
if service['slug'] in pages_by_slug: |
|
110 |
label = _('Page "%(page)s" and sub pages Contents') % {'page': pages_by_slug[service['slug']]} |
|
111 |
services.append((service['slug'], label)) |
|
112 |
return services |
|
113 | ||
82 | 114 |
@property |
83 | 115 |
def has_multiple_search_services(self): |
84 | 116 |
return len(self._search_services.get('data') or []) > 1 |
... | ... | |
124 | 156 | |
125 | 157 |
query = request.GET.get('q') |
126 | 158 | |
127 |
def render_response(service={}, results={'err': 0, 'data': []}): |
|
159 |
def render_response(service=None, results=None, pages=None): |
|
160 |
service = service or {} |
|
161 |
results = results or {'err': 0, 'data': []} |
|
128 | 162 |
template_names = ['combo/search-cell-results.html'] |
129 | 163 |
if cell.slug: |
130 | 164 |
template_names.insert(0, 'combo/cells/%s/search-cell-results.html' % cell.slug) |
131 | 165 |
tmpl = template.loader.select_template(template_names) |
166 |
service_label = service.get('label') |
|
167 |
if pages: |
|
168 |
service_label = _('Page "%(page)s" and sub pages Contents') % {'page': pages[0].title} |
|
132 | 169 |
context = { |
133 | 170 |
'cell': cell, |
134 | 171 |
'results': results, |
135 | 172 |
'search_service': service, |
173 |
'search_service_label': service_label, |
|
136 | 174 |
'query': query |
137 | 175 |
} |
138 | 176 |
return HttpResponse(tmpl.render(context, request), content_type='text/html') |
... | ... | |
146 | 184 |
if not query: |
147 | 185 |
return render_response(service) |
148 | 186 | |
187 |
pages = None |
|
149 | 188 |
if service.get('function'): # internal search engine |
150 |
results = {'data': service['function'](request, query)} |
|
189 |
pages = get_root_page_and_children(service_slug) |
|
190 |
results = {'data': service['function'](request, query, pages=pages)} |
|
151 | 191 |
else: |
152 | 192 |
url = get_templated_url(service['url'], |
153 | 193 |
context={'request': request, 'q': query, 'search_service': service}) |
... | ... | |
188 | 228 |
for k, v in hit_templates.items(): |
189 | 229 |
hit[k] = v.render(RequestContext(request, hit)) |
190 | 230 | |
191 |
return render_response(service, results) |
|
231 |
return render_response(service, results, pages=pages)
|
|
192 | 232 | |
193 | 233 |
def has_text_search_service(self): |
194 |
return '_text' in self._search_services.get('data', [])
|
|
234 |
return any(key.startswith('_text') for key in self._search_services.get('data', []))
|
|
195 | 235 | |
196 | 236 |
def missing_index(self): |
197 | 237 |
return IndexedCell.objects.all().count() == 0 |
combo/apps/search/templates/combo/manager/add-engine-form.html | ||
---|---|---|
1 |
{% extends "combo/manager_base.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block appbar %} |
|
5 |
<h2>{% trans 'Add a "Page Contents" engine' %}</h2> |
|
6 |
{% endblock %} |
|
7 | ||
8 |
{% block content %} |
|
9 | ||
10 |
<form method="post" enctype="multipart/form-data"> |
|
11 |
{% csrf_token %} |
|
12 |
{{ form.as_p }} |
|
13 |
<div class="buttons"> |
|
14 |
<button class="submit-button">{% trans "Save" %}</button> |
|
15 |
<a class="cancel" href="{% url 'combo-manager-page-view' pk=cell.page_id %}">{% trans 'Cancel' %}</a> |
|
16 |
</div> |
|
17 |
</form> |
|
18 |
{% endblock %} |
combo/apps/search/templates/combo/manager/search-cell-form.html | ||
---|---|---|
13 | 13 |
</div> |
14 | 14 |
{% endif %} |
15 | 15 |
{{ form.as_p }} |
16 |
{% with cell.search_services as engines %}
|
|
16 |
{% with cell.get_search_services_for_display as engines %}
|
|
17 | 17 |
{% if engines %} |
18 | 18 |
<p><label>{% trans "Engines:" %}</label></p> |
19 | 19 |
<div> |
20 | 20 |
<ul class="objects-list list-of-links" id="list-of-links-{{ cell.pk }}" |
21 | 21 |
data-link-list-order-url="{% url 'combo-manager-search-engines-order' page_pk=page.pk cell_reference=cell.get_reference %}"> |
22 | 22 |
{% for engine in engines %} |
23 |
<li data-link-item-id="{{ engine.slug }}"><span class="handle">⣿</span>
|
|
24 |
<span>{{ engine.label }}</span>
|
|
25 |
<a title="{% trans "Delete" %}" class="link-action-icon delete" href="{% url 'combo-manager-page-search-cell-delete-engine' page_pk=page.pk cell_reference=cell.get_reference engine_slug=engine.slug %}">{% trans "Delete" %}</a>
|
|
23 |
<li data-link-item-id="{{ engine.0 }}"><span class="handle">⣿</span>
|
|
24 |
<span>{{ engine.1 }}</span>
|
|
25 |
<a title="{% trans "Delete" %}" class="link-action-icon delete" href="{% url 'combo-manager-page-search-cell-delete-engine' page_pk=page.pk cell_reference=cell.get_reference engine_slug=engine.0 %}">{% trans "Delete" %}</a>
|
|
26 | 26 |
</li> |
27 | 27 |
{% endfor %} |
28 | 28 |
</ul> |
... | ... | |
51 | 51 |
<div class="buttons"> |
52 | 52 |
{% trans "Add an engine:" %} |
53 | 53 |
{% for key, engine in cell.available_engines.items %} |
54 |
<a href="{% url 'combo-manager-page-search-cell-add-engine' page_pk=page.pk cell_reference=cell.get_reference engine_slug=key %}">{{ engine.label }}</a> {% if not forloop.last %}|{% endif %} |
|
54 |
<a {% if key == '_text' %}rel="popup"{% endif %} href="{% url 'combo-manager-page-search-cell-add-engine' page_pk=page.pk cell_reference=cell.get_reference engine_slug=key %}">{{ engine.label }}</a> {% if not forloop.last %}|{% endif %}
|
|
55 | 55 |
{% endfor %} |
56 | 56 |
</div> |
57 | 57 |
{% endif %} |
combo/apps/search/templates/combo/search-cell-results.html | ||
---|---|---|
1 | 1 |
{% load i18n %} |
2 | 2 |
{% if query %} |
3 |
{% if cell.has_multiple_search_services %}<p class="search-service-label">{{ search_service.label }}</p>{% endif %}
|
|
3 |
{% if cell.has_multiple_search_services %}<p class="search-service-label">{{ search_service_label }}</p>{% endif %}
|
|
4 | 4 |
{% if results.data %} |
5 | 5 |
<div class="links-list"> |
6 | 6 |
<ul> |
combo/apps/search/utils.py | ||
---|---|---|
105 | 105 |
indexed_cell.save() |
106 | 106 | |
107 | 107 | |
108 |
def search_site(request, query): |
|
108 |
def search_site(request, query, pages=None): |
|
109 |
pages = pages or [] |
|
110 | ||
109 | 111 |
if connection.vendor == 'postgresql': |
110 | 112 |
config = settings.POSTGRESQL_FTS_SEARCH_CONFIG |
111 | 113 |
vector = SearchVector('title', config=config, weight='A') + SearchVector('indexed_text', config=config, weight='A') |
... | ... | |
122 | 124 |
Q(restricted_groups__in=request.user.groups.all())) |
123 | 125 |
qs = qs.exclude(excluded_groups__in=request.user.groups.all()) |
124 | 126 | |
127 |
if pages: |
|
128 |
qs = qs.filter(page__in=pages) |
|
129 | ||
125 | 130 |
hits = [] |
126 | 131 |
seen = {} |
127 | 132 |
for hit in qs: |
tests/test_search.py | ||
---|---|---|
15 | 15 |
from combo.apps.search.engines import engines |
16 | 16 |
from combo.apps.search.models import SearchCell, IndexedCell |
17 | 17 |
from combo.apps.search.utils import index_site, search_site |
18 |
from combo.data.models import Page, JsonCell, TextCell, MenuCell, LinkCell |
|
18 |
from combo.data.models import Page, JsonCell, TextCell, MenuCell, LinkCell, PageSnapshot
|
|
19 | 19 | |
20 | 20 |
from .test_manager import login |
21 | 21 | |
... | ... | |
312 | 312 |
assert resp.text.count('<li') == 0 |
313 | 313 | |
314 | 314 | |
315 |
def test_search_on_root_page_api(app): |
|
316 |
# not indexed: with sub_slug |
|
317 |
page = Page.objects.create(title='example page', slug='example-page', sub_slug='foo') |
|
318 |
TextCell.objects.create(page=page, placeholder='content', text='<p>foobar baz</p>', order=0) |
|
319 | ||
320 |
second_page = Page.objects.create(title='second page', slug='second-page') |
|
321 |
TextCell.objects.create(page=second_page, placeholder='content', text='<p>other baz</p>', order=0) |
|
322 |
sub_second_page = Page.objects.create(parent=second_page, title='sub second page', slug='sub-second-page') |
|
323 |
TextCell.objects.create(page=sub_second_page, placeholder='content', text='<p>other baz</p>', order=0) |
|
324 |
# not indexed: with snapshot |
|
325 |
third_page = Page.objects.create(title='second page', slug='third-page') |
|
326 |
TextCell.objects.create(page=third_page, placeholder='content', text='<p>other baz again</p>', order=0) |
|
327 |
third_page.snapshot = PageSnapshot.objects.create(page=third_page) |
|
328 |
third_page.save() |
|
329 | ||
330 |
index_site() |
|
331 | ||
332 |
cell = SearchCell.objects.create(page=page, placeholder='content', _search_services={'data': ['_text']}, order=1) |
|
333 | ||
334 |
resp = app.get('/ajax/search/%s/_text/?q=baz' % cell.pk, status=200) |
|
335 |
assert resp.text.count('<li') == 2 |
|
336 |
cell._search_services = {'data': ['_text_page_second-page']} |
|
337 |
cell.save() |
|
338 |
resp = app.get('/ajax/search/%s/_text_page_second-page/?q=baz' % cell.pk, status=200) |
|
339 |
assert resp.text.count('<li') == 2 |
|
340 |
cell._search_services = {'data': ['_text_page_sub-second-page']} |
|
341 |
cell.save() |
|
342 |
resp = app.get('/ajax/search/%s/_text_page_sub-second-page/?q=baz' % cell.pk, status=200) |
|
343 |
assert resp.text.count('<li') == 1 |
|
344 |
# invalid page, search everywhere |
|
345 |
# with sub_slug |
|
346 |
cell._search_services = {'data': ['_text_page_example-page']} |
|
347 |
cell.save() |
|
348 |
resp = app.get('/ajax/search/%s/_text_page_example-page/?q=baz' % cell.pk, status=200) |
|
349 |
assert resp.text.count('<li') == 2 |
|
350 |
# with snapshot |
|
351 |
cell._search_services = {'data': ['_text_page_third-page']} |
|
352 |
cell.save() |
|
353 |
resp = app.get('/ajax/search/%s/_text_page_third-page/?q=baz' % cell.pk, status=200) |
|
354 |
assert resp.text.count('<li') == 2 |
|
355 |
# page does not exists, search everywhere |
|
356 |
cell._search_services = {'data': ['_text_page_foo']} |
|
357 |
cell.save() |
|
358 |
resp = app.get('/ajax/search/%s/_text_page_foo/?q=baz' % cell.pk, status=200) |
|
359 |
assert resp.text.count('<li') == 2 |
|
360 |
# slug is not unique, search everywhere |
|
361 |
page.slug = 'sub-second-page' |
|
362 |
page.sub_slug = '' |
|
363 |
page.save() |
|
364 |
cell._search_services = {'data': ['_text_page_sub-second-page']} |
|
365 |
cell.save() |
|
366 |
resp = app.get('/ajax/search/%s/_text_page_sub-second-page/?q=baz' % cell.pk, status=200) |
|
367 |
assert resp.text.count('<li') == 2 |
|
368 | ||
369 | ||
315 | 370 |
def test_search_external_links(app): |
316 | 371 |
page = Page(title='example page', slug='example-page') |
317 | 372 |
page.save() |
... | ... | |
372 | 427 | |
373 | 428 |
# add engines |
374 | 429 |
resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk) |
430 |
resp = resp.form.submit('submit') |
|
375 | 431 |
assert resp.status_int == 302 |
376 | 432 |
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) |
377 | 433 |
resp = app.get('/manage/pages/%s/' % page.pk) |
... | ... | |
381 | 437 |
cell.refresh_from_db() |
382 | 438 |
assert cell._search_services == {'data': ['_text', 'search1', 'search_tmpl']} |
383 | 439 |
resp = app.get('/manage/pages/%s/' % page.pk) |
384 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) not in resp.text |
|
440 |
# '_text' is always available |
|
441 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) in resp.text |
|
385 | 442 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/add/' % (page.pk, cell.pk) not in resp.text |
386 | 443 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/add/' % (page.pk, cell.pk) not in resp.text |
387 | 444 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/add/' % (page.pk, cell.pk) in resp.text |
... | ... | |
407 | 464 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/delete/' % (page.pk, cell.pk) not in resp.text |
408 | 465 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/delete/' % (page.pk, cell.pk) not in resp.text |
409 | 466 | |
467 |
# add engines on page and sub pages |
|
468 |
resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk) |
|
469 |
assert list(resp.context['form']['selected_page'].field.queryset) == [page] |
|
470 |
resp = resp.form.submit('submit') |
|
471 |
assert resp.status_int == 302 |
|
472 |
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) |
|
473 |
cell.refresh_from_db() |
|
474 |
assert cell._search_services == {'data': ['search1', 'search_tmpl', '_text']} |
|
475 |
resp = app.get('/manage/pages/%s/' % page.pk) |
|
476 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) in resp.text |
|
477 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/delete/' % (page.pk, cell.pk) in resp.text |
|
478 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text_page_one/delete/' % (page.pk, cell.pk) not in resp.text |
|
479 |
resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk) |
|
480 |
resp.form['selected_page'] = page.pk |
|
481 |
resp = resp.form.submit('submit') |
|
482 |
assert resp.status_int == 302 |
|
483 |
assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference())) |
|
484 |
cell.refresh_from_db() |
|
485 |
assert cell._search_services == {'data': ['search1', 'search_tmpl', '_text', '_text_page_one']} |
|
486 |
resp = app.get('/manage/pages/%s/' % page.pk) |
|
487 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) in resp.text |
|
488 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/delete/' % (page.pk, cell.pk) in resp.text |
|
489 |
assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text_page_one/delete/' % (page.pk, cell.pk) in resp.text |
|
490 | ||
410 | 491 | |
411 | 492 |
def test_manager_search_cell_order(settings, app, admin_user): |
412 | 493 |
settings.COMBO_SEARCH_SERVICES = SEARCH_SERVICES |
... | ... | |
435 | 516 |
assert 'Content indexing has been scheduled' not in resp.text |
436 | 517 | |
437 | 518 |
resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk) |
519 |
resp = resp.form.submit('submit') |
|
438 | 520 |
resp = app.get('/manage/pages/%s/' % page.pk) |
439 | 521 |
assert 'Content indexing has been scheduled' in resp.text |
440 | 522 | |
441 |
- |