Projet

Général

Profil

0002-search-search-on-page-and-subpages-40224.patch

Lauréline Guérin, 06 avril 2020 16:41

Télécharger (21,4 ko)

Voir les différences:

Subject: [PATCH 2/2] search: search on page and subpages (#40224)

 combo/apps/search/forms.py                    | 30 +++++++
 combo/apps/search/manager_views.py            | 41 ++++++---
 combo/apps/search/models.py                   | 50 +++++++++--
 .../combo/manager/add-engine-form.html        | 18 ++++
 .../combo/manager/search-cell-form.html       | 10 +--
 .../templates/combo/search-cell-results.html  |  2 +-
 combo/apps/search/utils.py                    |  7 +-
 tests/test_search.py                          | 86 ++++++++++++++++++-
 8 files changed, 220 insertions(+), 24 deletions(-)
 create mode 100644 combo/apps/search/templates/combo/manager/add-engine-form.html
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
-