Projet

Général

Profil

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

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

Télécharger (20,3 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                          | 135 +++++++++++-------
 6 files changed, 228 insertions(+), 67 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')
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', )
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

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

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

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

  
77 82
    @property
78 83
    def has_multiple_search_services(self):
79 84
        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
......
215 213
            assert '<li><a href="http://example.net/123/">A B</a>' in resp.text
216 214
            assert '<div>description A</div>' in resp.text
217 215

  
216

  
218 217
def test_search_cell_visibility(app):
219 218
    page = Page(title='example page', slug='example-page')
220 219
    page.save()
......
224 223
        assert not cell.is_visible()
225 224

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

  
229

  
229 230
def test_search_contents():
230 231
    page = Page(title='example page', slug='example-page')
231 232
    page.save()
......
347 348
    assert hits[0]['url'] == 'http://example.net'
348 349

  
349 350

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

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

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

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

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

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

  
410

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

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

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

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

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

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

  
391 429

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

  
403
    cells = Page.objects.get(id=page.id).get_cells()
404
    resp.form['c%s-_search_services' % cells[0].get_reference()] = ['_text']
405
    resp = resp.form.submit().follow()
437
    resp = resp.click(href='.*/search_searchcell-%s/engine/_text/add/' % cell.pk)
438
    resp = app.get('/manage/pages/%s/' % page.pk)
406 439
    assert 'Content indexing has been scheduled' in resp.text
407 440

  
408 441
    index_site()
409
    resp = app.get('/manage/pages/%s/' % page.id)
442
    resp = app.get('/manage/pages/%s/' % page.pk)
410 443
    assert 'Content indexing has been scheduled' not in resp.text
411 444

  
412 445

  
413
-