Projet

Général

Profil

0001-search-new-manager-selection-SearchCell-engines-4022.patch

Lauréline Guérin, 17 avril 2020 11:33

Télécharger (20,6 ko)

Voir les différences:

Subject: [PATCH 1/2] search: new manager selection SearchCell engines (#40224)

 combo/apps/search/forms.py                    |  11 +-
 combo/apps/search/manager_views.py            |  75 ++++++++++
 combo/apps/search/models.py                   |  11 +-
 .../combo/manager/search-cell-form.html       |  44 +++++-
 combo/apps/search/urls.py                     |  19 ++-
 tests/test_search.py                          | 139 +++++++++++-------
 6 files changed, 228 insertions(+), 71 deletions(-)
 create mode 100644 combo/apps/search/manager_views.py
combo/apps/search/forms.py
16 16

  
17 17
from django import forms
18 18

  
19
from combo.utils.forms import MultiSortWidget
20

  
21
from . import engines
22 19
from .models import SearchCell
23 20

  
24 21

  
25 22
class SearchCellForm(forms.ModelForm):
26 23
    class Meta:
27 24
        model = SearchCell
28
        fields = ('_search_services', 'autofocus', 'input_placeholder')
29

  
30
    def __init__(self, *args, **kwargs):
31
        super(SearchCellForm, self).__init__(*args, **kwargs)
32
        options = [(x, engines.get_engines()[x]['label']) for x in engines.get_engines().keys()]
33
        self.fields['_search_services'].widget = MultiSortWidget(choices=options,
34
                                                                 with_checkboxes=True)
25
        fields = ('autofocus', 'input_placeholder')
combo/apps/search/manager_views.py
1
# combo - content management system
2
# Copyright (C) 2014-2020  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.http import HttpResponse
18
from django.http import HttpResponseRedirect
19
from django.shortcuts import get_object_or_404
20
from django.urls import reverse
21
from django.utils.translation import ugettext_lazy as _
22

  
23
from combo.apps.search.models import SearchCell
24
from combo.data.models import PageSnapshot
25

  
26

  
27
def page_search_cell_add_engine(request, page_pk, cell_reference, engine_slug):
28
    cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
29

  
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))
40

  
41

  
42
def page_search_cell_delete_engine(request, page_pk, cell_reference, engine_slug):
43
    cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
44

  
45
    if engine_slug in cell._search_services.get('data'):
46
        cell._search_services['data'].remove(engine_slug)
47
        cell.save()
48
        PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
49

  
50
    return HttpResponseRedirect('%s#cell-%s' % (
51
        reverse('combo-manager-page-view', kwargs={'pk': page_pk}),
52
        cell_reference))
53

  
54

  
55
def search_engines_order(request, page_pk, cell_reference):
56
    cell = get_object_or_404(SearchCell, pk=cell_reference.split('-')[1], page=page_pk)
57

  
58
    if not cell._search_services.get('data'):
59
        return HttpResponse(status=204)
60

  
61
    engines = []
62
    for i, engine_slug in enumerate(cell._search_services['data']):
63
        try:
64
            new_order = int(request.GET.get('pos_' + str(engine_slug)))
65
        except TypeError:
66
            new_order = 0
67
        engines.append((new_order, engine_slug))
68

  
69
    ordered_engines = [a[1] for a in sorted(engines, key=lambda a: a[0])]
70
    if ordered_engines != cell._search_services['data']:
71
        cell._search_services['data'] = ordered_engines
72
        cell.save()
73
        PageSnapshot.take(cell.page, request=request, comment=_('changed cell "%s"') % cell)
74

  
75
    return HttpResponse(status=204)
combo/apps/search/models.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
import os
18

  
19 17
from django.contrib.auth.models import Group
20 18
from django.contrib.contenttypes import fields
21 19
from django.contrib.contenttypes.models import ContentType
......
24 22
from django import template
25 23
from django.http import HttpResponse
26 24
from django.core.exceptions import PermissionDenied
25
from django.utils.functional import cached_property
27 26
from django.utils.http import quote
28 27
from django.template import RequestContext, Template
29 28

  
......
65 64
            return self.slug.replace('-', '_')
66 65
        return ''
67 66

  
68
    @property
67
    @cached_property
69 68
    def search_services(self):
70 69
        services = []
71 70
        for service_slug in self._search_services.get('data') or []:
......
75 74
                services.append(service)
76 75
        return services
77 76

  
77
    @cached_property
78
    def available_engines(self):
79
        all_engines = engines.get_engines()
80
        current_engines = [e['slug'] for e in self.search_services]
81
        return {k: v for k, v in all_engines.items() if k not in current_engines}
82

  
78 83
    @property
79 84
    def has_multiple_search_services(self):
80 85
        return len(self._search_services.get('data') or []) > 1
combo/apps/search/templates/combo/manager/search-cell-form.html
12 12
  </p>
13 13
</div>
14 14
{% endif %}
15
{{ block.super }}
15
{{ form.as_p }}
16
{% with cell.search_services as engines %}
17
{% if engines %}
18
<p><label>{% trans "Engines:" %}</label></p>
19
<div>
20
  <ul class="objects-list list-of-links" id="list-of-links-{{ cell.pk }}"
21
     data-link-list-order-url="{% url 'combo-manager-search-engines-order' page_pk=page.pk cell_reference=cell.get_reference %}">
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>
26
    </li>
27
    {% endfor %}
28
  </ul>
29
</div>
30
<script>
31
$(function () {
32
  $('#list-of-links-{{ cell.pk }}').sortable({
33
    handle: '.handle',
34
    update: function(event, ui) {
35
       var new_order = Object();
36
       $(this).find('li').each(function(i, x) {
37
           var suffix = $(x).data('link-item-id');
38
           new_order['pos_' + suffix] = i;
39
       });
40
       $.ajax({
41
           url: $(this).data('link-list-order-url'),
42
           data: new_order
43
       });
44
    }
45
  });
46
});
47
</script>
48
{% endif %}
49
{% endwith %}
50
{% if cell.available_engines %}
51
<div class="buttons">
52
  {% trans "Add an engine:" %}
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 %}
55
  {% endfor %}
56
</div>
57
{% endif %}
16 58
{% endblock %}
combo/apps/search/urls.py
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17
from django.conf.urls import url
17
from django.conf.urls import url, include
18 18

  
19
from combo.urls_utils import decorated_includes, manager_required
19 20
from .models import SearchCell
21
from . import manager_views
22

  
23
search_manager_urls = [
24
    url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_]+-\d+)/engine/(?P<engine_slug>[\w_-]+)/add/$',
25
        manager_views.page_search_cell_add_engine,
26
        name='combo-manager-page-search-cell-add-engine'),
27
    url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_]+-\d+)/engine/(?P<engine_slug>[\w_-]+)/delete/$',
28
        manager_views.page_search_cell_delete_engine,
29
        name='combo-manager-page-search-cell-delete-engine'),
30
    url(r'^pages/(?P<page_pk>\d+)/cell/(?P<cell_reference>[\w_]+-\d+)/engine/order$',
31
        manager_views.search_engines_order,
32
        name='combo-manager-search-engines-order'),
33
]
20 34

  
21 35
urlpatterns = [
22
    url(r'^ajax/search/(?P<cell_pk>\w+)/(?P<service_slug>[\w:-]+)/$', SearchCell.ajax_results_view,
36
    url(r'^manage/search/', decorated_includes(manager_required, include(search_manager_urls))),
37
    url(r'^ajax/search/(?P<cell_pk>\w+)/(?P<service_slug>[\w_-]+)/$', SearchCell.ajax_results_view,
23 38
        name='combo-search-ajax-results'),
24 39
]
tests/test_search.py
1 1
import json
2 2
import os
3 3
import pytest
4
import re
5
import shutil
6 4
import mock
7 5

  
8 6
from django.conf import settings
......
11 9
from django.test import override_settings
12 10
from django.test.client import RequestFactory
13 11
from django.test.utils import CaptureQueriesContext
14
from django.core.management import call_command
15 12
from django.urls import reverse
13
from django.utils.http import urlencode
16 14

  
17 15
from combo.apps.search.engines import engines
18 16
from combo.apps.search.models import SearchCell, IndexedCell
......
217 215
            assert '<li><a href="http://example.net/123/">A B</a>' in resp.text
218 216
            assert '<div>description A</div>' in resp.text
219 217

  
218

  
220 219
def test_search_cell_visibility(app):
221 220
    page = Page(title='example page', slug='example-page')
222 221
    page.save()
......
226 225
        assert not cell.is_visible()
227 226

  
228 227
        cell._search_services = {'data': ['_text']}
228
        del cell.search_services  # clear cache
229 229
        assert cell.is_visible()
230 230

  
231

  
231 232
def test_search_contents():
232 233
    page = Page(title='example page', slug='example-page')
233 234
    page.save()
......
349 350
    assert hits[0]['url'] == 'http://example.net'
350 351

  
351 352

  
352
def test_manager_search_cell(app, admin_user):
353
    Page.objects.all().delete()
354
    page = Page(title='One', slug='one', template_name='standard')
355
    page.save()
353
def test_manager_search_cell(settings, app, admin_user):
354
    page = Page.objects.create(title='One', slug='one', template_name='standard')
355
    cell = SearchCell.objects.create(page=page, placeholder='content', order=0)
356 356
    app = login(app)
357
    resp = app.get('/manage/pages/%s/' % page.id)
358
    resp = app.get(resp.html.find('option',
359
                   **{'data-add-url': re.compile('search_searchcell')})['data-add-url'])
360 357

  
361
    cells = Page.objects.get(id=page.id).get_cells()
362
    assert len(cells) == 1
363
    assert isinstance(cells[0], SearchCell)
358
    settings.KNOWN_SERVICES = {}
359
    assert cell._search_services == {}
360
    resp = app.get('/manage/pages/%s/' % page.pk)
361
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) in resp.text
362
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/delete/' % (page.pk, cell.pk) not in resp.text
363

  
364
    settings.COMBO_SEARCH_SERVICES = SEARCH_SERVICES
365
    resp = app.get('/manage/pages/%s/' % page.pk)
366
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) in resp.text
367
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/add/' % (page.pk, cell.pk) in resp.text
368
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/add/' % (page.pk, cell.pk) in resp.text
369
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/add/' % (page.pk, cell.pk) in resp.text
370
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/delete/' % (page.pk, cell.pk) not in resp.text
371
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/delete/' % (page.pk, cell.pk) not in resp.text
372
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/delete/' % (page.pk, cell.pk) not in resp.text
373
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/delete/' % (page.pk, cell.pk) not in resp.text
374

  
375
    # add engines
376
    resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk)
377
    assert resp.status_int == 302
378
    assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference()))
379
    resp = app.get('/manage/pages/%s/' % page.pk)
380
    resp = resp.click(href='.*/search_searchcell-%s/engine/search1/add/' % cell.pk)
381
    resp = app.get('/manage/pages/%s/' % page.pk)
382
    resp = resp.click(href='.*/search_searchcell-%s/engine/search_tmpl/add/' % cell.pk)
383
    cell.refresh_from_db()
384
    assert cell._search_services == {'data': ['_text', 'search1', 'search_tmpl']}
385
    resp = app.get('/manage/pages/%s/' % page.pk)
386
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/add/' % (page.pk, cell.pk) not in resp.text
387
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/add/' % (page.pk, cell.pk) not in resp.text
388
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/add/' % (page.pk, cell.pk) not in resp.text
389
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/add/' % (page.pk, cell.pk) in resp.text
390
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/_text/delete/' % (page.pk, cell.pk) in resp.text
391
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/delete/' % (page.pk, cell.pk) in resp.text
392
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/delete/' % (page.pk, cell.pk) in resp.text
393
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/delete/' % (page.pk, cell.pk) not in resp.text
394

  
395
    # delete engines
396
    resp = resp.click(href='.*/search_searchcell-%s/engine/_text/delete/' % cell.pk)
397
    assert resp.status_int == 302
398
    assert resp.location.endswith('/manage/pages/%s/#cell-%s' % (page.pk, cell.get_reference()))
399
    cell.refresh_from_db()
400
    assert cell._search_services == {'data': ['search1', 'search_tmpl']}
401

  
402
    settings.COMBO_SEARCH_SERVICES = {}
403
    # check there's no crash if search engines are removed from config
404
    resp = app.get('/manage/pages/%s/' % page.pk)
405
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/add/' % (page.pk, cell.pk) not in resp.text
406
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/add/' % (page.pk, cell.pk) not in resp.text
407
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/add/' % (page.pk, cell.pk) not in resp.text
408
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search1/delete/' % (page.pk, cell.pk) not in resp.text
409
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_tmpl/delete/' % (page.pk, cell.pk) not in resp.text
410
    assert '/manage/search/pages/%s/cell/search_searchcell-%s/engine/search_alternate_key/delete/' % (page.pk, cell.pk) not in resp.text
411

  
412

  
413
def test_manager_search_cell_order(settings, app, admin_user):
414
    settings.COMBO_SEARCH_SERVICES = SEARCH_SERVICES
415
    page = Page.objects.create(title='One', slug='one', template_name='standard')
416
    cell = SearchCell.objects.create(
417
        page=page, placeholder='content', order=0,
418
        _search_services={'data': ['_text', 'search1', 'search_tmpl']})
419

  
420
    params = []
421
    new_order = [2, 3, 1]
422
    for i, (service_slug, new_pos) in enumerate(zip(cell._search_services['data'], new_order)):
423
        params.append(('pos_%s' % service_slug, str(new_pos)))
364 424

  
365
    with override_settings(KNOWN_SERVICES={}):
366
        resp = app.get('/manage/pages/%s/' % page.id)
367
        assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
368
        assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 1
369

  
370
        with SearchServices(SEARCH_SERVICES):
371
            resp = app.get('/manage/pages/%s/' % page.id)
372
            assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 4
373
            # simulate reordering of options
374
            resp.form['c%s-_search_services' % cells[0].get_reference()].options = [
375
                    (u'search_tmpl', False, u'Search with template'),
376
                    (u'search_alternate_key', False, u'Search with alternate key'),
377
                    (u'_text', False, u'Page Contents'),
378
                    (u'search1', False, u'Search 1')]
379
            resp.form['c%s-_search_services' % cells[0].get_reference()].value = ['search_tmpl', '_text']
380
            resp.form['c%s-input_placeholder' % cells[0].get_reference()] = 'my placeholder'
381
            resp = resp.form.submit()
382
            assert resp.status_int == 302
383

  
384
            # check selected engines are selected and the first items of the list
385
            resp = app.get('/manage/pages/%s/' % page.id)
386
            assert set(resp.form['c%s-_search_services' % cells[0].get_reference()].value) == set(['search_tmpl', '_text'])
387
            assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[0][0] == 'search_tmpl'
388
            assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[1][0] == '_text'
389

  
390
            # check placeholder
391
            resp.form['c%s-input_placeholder' % cells[0].get_reference()] == 'my placeholder'
392

  
393
        # check there's no crash if search engines are removed from config
394
        resp = app.get('/manage/pages/%s/' % page.id)
395
        assert resp.form['c%s-_search_services' % cells[0].get_reference()].value == ['_text']
425
    app = login(app)
426
    resp = app.get('/manage/search/pages/%s/cell/%s/engine/order?%s' % (page.pk, cell.get_reference(), urlencode(params)))
427
    assert resp.status_code == 204
428
    cell.refresh_from_db()
429
    assert cell._search_services == {'data': ['search_tmpl', '_text', 'search1']}
396 430

  
397 431

  
398 432
def test_manager_waiting_index_message(app, admin_user):
399
    Page.objects.all().delete()
400
    page = Page(title='One', slug='one', template_name='standard')
401
    page.save()
433
    page = Page.objects.create(title='One', slug='one', template_name='standard')
434
    cell = SearchCell.objects.create(page=page, placeholder='content', order=0)
402 435
    app = login(app)
403
    resp = app.get('/manage/pages/%s/' % page.id)
404
    resp = app.get(resp.html.find('option',
405
                   **{'data-add-url': re.compile('search_searchcell')})['data-add-url'])
406
    resp = resp.follow()
436
    resp = app.get('/manage/pages/%s/' % page.pk)
407 437
    assert 'Content indexing has been scheduled' not in resp.text
408 438

  
409
    cells = Page.objects.get(id=page.id).get_cells()
410
    resp.form['c%s-_search_services' % cells[0].get_reference()] = ['_text']
411
    resp = resp.form.submit().follow()
439
    resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk)
440
    resp = app.get('/manage/pages/%s/' % page.pk)
412 441
    assert 'Content indexing has been scheduled' in resp.text
413 442

  
414 443
    index_site()
415
    resp = app.get('/manage/pages/%s/' % page.id)
444
    resp = app.get('/manage/pages/%s/' % page.pk)
416 445
    assert 'Content indexing has been scheduled' not in resp.text
417 446

  
418 447

  
419
-