0003-search-add-custom-widget-to-sort-and-enable-engines-.patch
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 |
- |