Projet

Général

Profil

0003-search-add-custom-widget-to-sort-and-enable-engines-.patch

Thomas Noël, 27 août 2018 11:15

Télécharger (13,8 ko)

Voir les différences:

Subject: [PATCH 03/11] search: add custom widget to sort and enable engines
 (#23534)

 combo/apps/search/engines.py             |  8 +++
 combo/apps/search/forms.py               | 34 ++++++++++++
 combo/apps/search/models.py              |  4 ++
 combo/apps/wcs/forms.py                  | 42 ++-------------
 combo/manager/static/js/combo.manager.js | 48 +++++++++++++----
 combo/utils/forms.py                     | 69 ++++++++++++++++++++++++
 tests/test_search.py                     | 43 +++++++++++++++
 7 files changed, 200 insertions(+), 48 deletions(-)
 create mode 100644 combo/apps/search/forms.py
 create mode 100644 combo/utils/forms.py
combo/apps/search/engines.py
32 32
            return settings.COMBO_SEARCH_SERVICES[key]
33 33
        return self.engines.get(key)
34 34

  
35
    def get_engines(self):
36
        data = {}
37
        for key in settings.COMBO_SEARCH_SERVICES:
38
            data[key] = settings.COMBO_SEARCH_SERVICES[key]
39
        for key in self.engines:
40
            data[key] = self.engines[key]
41
        return data
42

  
35 43
engines = Engines()  # singleton object
combo/apps/search/forms.py
1
# combo - content management system
2
# Copyright (C) 2014-2018  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 import forms
18

  
19
from combo.utils.forms import MultiSortWidget
20

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

  
24

  
25
class SearchCellForm(forms.ModelForm):
26
    class Meta:
27
        model = SearchCell
28
        fields = ('_search_services',)
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)
combo/apps/search/models.py
46 46
            return False
47 47
        return super(SearchCell, self).is_visible(user=user)
48 48

  
49
    def get_default_form_class(self):
50
        from .forms import SearchCellForm
51
        return SearchCellForm
52

  
49 53
    @property
50 54
    def varname(self):
51 55
        if self.slug:
combo/apps/wcs/forms.py
20 20
from django.utils.safestring import mark_safe
21 21
from django.utils.translation import ugettext_lazy as _
22 22

  
23
from combo.utils.forms import MultiSortWidget
24

  
23 25
from .models import (WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell,
24 26
                     WcsCurrentFormsCell)
25 27
from .utils import get_wcs_options, get_wcs_services
26 28

  
29

  
27 30
class WcsFormCellForm(forms.ModelForm):
28 31
    class Meta:
29 32
        model = WcsFormCell
......
46 49
        self.fields['category_reference'].widget = forms.Select(choices=references)
47 50

  
48 51

  
49

  
50
class MultiSortWidget(forms.SelectMultiple):
51
    def render(self, name, value, attrs=None, choices=()):
52
        # reorder choices to get them in the current value order
53
        self_choices = self.choices[:]
54
        choices_dict = dict(self_choices)
55
        if value:
56
            for option in reversed(value.get('data')):
57
                if not option in choices_dict:
58
                    continue
59
                option_tuple = (option, choices_dict[option])
60
                self.choices.remove(option_tuple)
61
                self.choices.insert(0, option_tuple)
62

  
63
        # render the <select multiple>
64
        if django.VERSION < (1, 11, 0):
65
            rendered = super(MultiSortWidget, self).render(name, value,
66
                    attrs=attrs, choices=choices)
67
        else:
68
            rendered = super(MultiSortWidget, self).render(name, value,
69
                    attrs=attrs)
70

  
71
        # include it in a <div> that will be turned into an appropriate widget
72
        # in javascript
73
        id_ = 'wid-%s' % name
74
        return mark_safe('''<div class="multisort" id="%s">%s</div>
75
        <script type="text/javascript">multisort($("#%s"));</script>
76
        ''' % (id_, rendered, id_))
77

  
78
    def render_options(self, choices, value):
79
        value = value.get('data') or []
80
        return super(MultiSortWidget, self).render_options(choices, value)
81

  
82
    def value_from_datadict(self, data, files, name):
83
        if isinstance(data, MultiValueDict):
84
            return {'data': data.getlist(name)}
85
        return data.get(name, None)
86

  
87

  
88 52
class WcsFormsOfCategoryCellForm(forms.ModelForm):
89 53
    class Meta:
90 54
        model = WcsFormsOfCategoryCell
combo/manager/static/js/combo.manager.js
27 27

  
28 28
  var $ul = $('<ul class="multisort"></ul>');
29 29

  
30
  var checkboxes = $(element).data('checkboxes');
31

  
30 32
  $(element).find('option').each(function(i, x) {
31 33
    if (category_value && $(x).val().indexOf(category_value + ':') != 0) {
32 34
      return;
33 35
    }
34
    $('<li data-value="' + $(x).val() + '"><span class="handle">⣿</span>' + $(x).text() + '</li>').appendTo($ul);
36
    var checkbox = '';
37
    if (checkboxes) {
38
      if ($(x).attr('selected')) {
39
        checkbox = '<input type="checkbox" checked/>'
40
      } else {
41
        checkbox = '<input type="checkbox"/>'
42
      }
43
    }
44
    $('<li data-value="' + $(x).val() + '"><span class="handle">⣿</span>'+ checkbox + $(x).text() + '</li>').appendTo($ul);
35 45
  });
36 46
  $ul.appendTo(element);
47

  
48
  function multisort_sync() {
49
    var $select = $(element).find('select');
50
    var options = Array();
51
    $ul.find('li').each(function(i, x) {
52
      var selected = true;
53
      if (checkboxes && $(x).find('input[type=checkbox]:checked').length == 0) {
54
        selected = false;
55
      }
56
      var value = $(x).data('value');
57
      var $option = $select.find('[value="' + value + '"]');
58
      if (selected) {
59
        $option.prop('selected', 'selected');
60
      } else {
61
        $option.prop('selected', null);
62
      }
63
      $option.detach();
64
      options.push($option);
65
    });
66
    while (options.length) {
67
      $select.prepend(options.pop());
68
    }
69
  }
70

  
71
  $ul.find('input[type=checkbox]').on('change', function() {
72
    multisort_sync();
73
  });
37 74
  $ul.sortable({
38 75
    handle: '.handle',
39 76
    update: function(event, ui) {
40
      var options = Array();
41
      var select = $(element).find('select');
42
      $ul.find('li').each(function(i, x) {
43
        options.push($(element).find("option[value='" + $(x).data('value') + "']").attr('selected', 'selected').detach());
44
      });
45
      while (options.length) {
46
        select.prepend(options.pop());
47
      }
77
      multisort_sync();
48 78
    }
49 79
  });
50 80
}
combo/utils/forms.py
1
# combo - content management system
2
# Copyright (C) 2014-2018  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
import django
18
from django import forms
19
from django.utils.datastructures import MultiValueDict
20
from django.utils.safestring import mark_safe
21

  
22

  
23
class MultiSortWidget(forms.SelectMultiple):
24
    def __init__(self, *args, **kwargs):
25
        if 'with_checkboxes' in kwargs:
26
            self.with_checkboxes = kwargs.pop('with_checkboxes')
27
        else:
28
            self.with_checkboxes = False
29
        super(MultiSortWidget, self).__init__(*args, **kwargs)
30

  
31
    def render(self, name, value, attrs=None, choices=()):
32
        # reorder choices to get them in the current value order
33
        self_choices = self.choices[:]
34
        choices_dict = dict(self_choices)
35
        if value:
36
            for option in reversed(value.get('data')):
37
                if option not in choices_dict:
38
                    continue
39
                option_tuple = (option, choices_dict[option])
40
                self.choices.remove(option_tuple)
41
                self.choices.insert(0, option_tuple)
42

  
43
        # render the <select multiple>
44
        if django.VERSION < (1, 11, 0):
45
            rendered = super(MultiSortWidget, self).render(name, value,
46
                    attrs=attrs, choices=choices)
47
        else:
48
            rendered = super(MultiSortWidget, self).render(name, value,
49
                    attrs=attrs)
50

  
51
        # include it in a <div> that will be turned into an appropriate widget
52
        # in javascript
53
        id_ = 'wid-%s' % name
54
        if self.with_checkboxes:
55
            attrs = 'data-checkboxes="true"'
56
        else:
57
            attrs = ''
58
        return mark_safe('''<div class="multisort" %s id="%s">%s</div>
59
        <script type="text/javascript">multisort($("#%s"));</script>
60
        ''' % (attrs, id_, rendered, id_))
61

  
62
    def render_options(self, choices, value):
63
        value = value.get('data') or []
64
        return super(MultiSortWidget, self).render_options(choices, value)
65

  
66
    def value_from_datadict(self, data, files, name):
67
        if isinstance(data, MultiValueDict):
68
            return {'data': data.getlist(name)}
69
        return data.get(name, None)
tests/test_search.py
1 1
import json
2 2
import os
3 3
import pytest
4
import re
4 5
import mock
5 6

  
6 7
from django.conf import settings
......
15 16
from combo.data.models import Page, JsonCell, TextCell, MenuCell, LinkCell
16 17
from combo.data.search_indexes import PageIndex
17 18

  
19
from .test_manager import admin_user, login
20

  
21

  
18 22
pytestmark = pytest.mark.django_db
19 23

  
20 24

  
......
276 280
    resp = app.get('/api/search/?q=bar', status=200)
277 281
    assert len(resp.json['data']) == 1
278 282
    assert resp.json['data'][0]['url'] == 'http://example.net'
283

  
284
def test_manager_search_cell(app, admin_user):
285
    Page.objects.all().delete()
286
    page = Page(title='One', slug='one', template_name='standard')
287
    page.save()
288
    app = login(app)
289
    resp = app.get('/manage/pages/%s/' % page.id)
290
    resp = app.get(resp.html.find('option',
291
                   **{'data-add-url': re.compile('search_searchcell')})['data-add-url'])
292

  
293
    cells = Page.objects.get(id=page.id).get_cells()
294
    assert len(cells) == 1
295
    assert isinstance(cells[0], SearchCell)
296

  
297
    resp = app.get('/manage/pages/%s/' % page.id)
298
    assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
299
    assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 1
300

  
301
    with SearchServices(SEARCH_SERVICES):
302
        resp = app.get('/manage/pages/%s/' % page.id)
303
        assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 3
304
        # simulate reordering of options
305
        resp.form['c%s-_search_services' % cells[0].get_reference()].options = [
306
                (u'search_tmpl', False, u'Search with template'),
307
                (u'_text', False, u'Page Contents'),
308
                (u'search1', False, u'Search 1')]
309
        resp.form['c%s-_search_services' % cells[0].get_reference()].value = ['search_tmpl', '_text']
310
        resp = resp.form.submit()
311
        assert resp.status_int == 302
312

  
313
        # check selected engines are selected and the first items of the list
314
        resp = app.get('/manage/pages/%s/' % page.id)
315
        assert set(resp.form['c%s-_search_services' % cells[0].get_reference()].value) == set(['search_tmpl', '_text'])
316
        assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[0][0] == 'search_tmpl'
317
        assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[1][0] == '_text'
318

  
319
    # check there's no crash if search engines are removed from config
320
    resp = app.get('/manage/pages/%s/' % page.id)
321
    assert resp.form['c%s-_search_services' % cells[0].get_reference()].value == ['_text']
279
-