search: add custom widget to sort and enable engines (#23534)

This commit is contained in:
Frédéric Péters 2018-08-11 10:35:49 +02:00 committed by Thomas NOEL
parent c0031f62d2
commit 28a2dab203
7 changed files with 200 additions and 48 deletions

View File

@ -32,4 +32,12 @@ class Engines(object):
return settings.COMBO_SEARCH_SERVICES[key]
return self.engines.get(key)
def get_engines(self):
data = {}
for key in settings.COMBO_SEARCH_SERVICES:
data[key] = settings.COMBO_SEARCH_SERVICES[key]
for key in self.engines:
data[key] = self.engines[key]
return data
engines = Engines() # singleton object

View File

@ -0,0 +1,34 @@
# combo - content management system
# Copyright (C) 2014-2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms
from combo.utils.forms import MultiSortWidget
from . import engines
from .models import SearchCell
class SearchCellForm(forms.ModelForm):
class Meta:
model = SearchCell
fields = ('_search_services',)
def __init__(self, *args, **kwargs):
super(SearchCellForm, self).__init__(*args, **kwargs)
options = [(x, engines.get_engines()[x]['label']) for x in engines.get_engines().keys()]
self.fields['_search_services'].widget = MultiSortWidget(choices=options,
with_checkboxes=True)

View File

@ -46,6 +46,10 @@ class SearchCell(CellBase):
return False
return super(SearchCell, self).is_visible(user=user)
def get_default_form_class(self):
from .forms import SearchCellForm
return SearchCellForm
@property
def varname(self):
if self.slug:

View File

@ -20,10 +20,13 @@ from django.utils.datastructures import MultiValueDict
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from combo.utils.forms import MultiSortWidget
from .models import (WcsFormCell, WcsCategoryCell, WcsFormsOfCategoryCell,
WcsCurrentFormsCell)
from .utils import get_wcs_options, get_wcs_services
class WcsFormCellForm(forms.ModelForm):
class Meta:
model = WcsFormCell
@ -46,45 +49,6 @@ class WcsCategoryCellForm(forms.ModelForm):
self.fields['category_reference'].widget = forms.Select(choices=references)
class MultiSortWidget(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
# reorder choices to get them in the current value order
self_choices = self.choices[:]
choices_dict = dict(self_choices)
if value:
for option in reversed(value.get('data')):
if not option in choices_dict:
continue
option_tuple = (option, choices_dict[option])
self.choices.remove(option_tuple)
self.choices.insert(0, option_tuple)
# render the <select multiple>
if django.VERSION < (1, 11, 0):
rendered = super(MultiSortWidget, self).render(name, value,
attrs=attrs, choices=choices)
else:
rendered = super(MultiSortWidget, self).render(name, value,
attrs=attrs)
# include it in a <div> that will be turned into an appropriate widget
# in javascript
id_ = 'wid-%s' % name
return mark_safe('''<div class="multisort" id="%s">%s</div>
<script type="text/javascript">multisort($("#%s"));</script>
''' % (id_, rendered, id_))
def render_options(self, choices, value):
value = value.get('data') or []
return super(MultiSortWidget, self).render_options(choices, value)
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
return {'data': data.getlist(name)}
return data.get(name, None)
class WcsFormsOfCategoryCellForm(forms.ModelForm):
class Meta:
model = WcsFormsOfCategoryCell

View File

@ -27,24 +27,54 @@ function multisort(element)
var $ul = $('<ul class="multisort"></ul>');
var checkboxes = $(element).data('checkboxes');
$(element).find('option').each(function(i, x) {
if (category_value && $(x).val().indexOf(category_value + ':') != 0) {
return;
}
$('<li data-value="' + $(x).val() + '"><span class="handle">⣿</span>' + $(x).text() + '</li>').appendTo($ul);
var checkbox = '';
if (checkboxes) {
if ($(x).attr('selected')) {
checkbox = '<input type="checkbox" checked/>'
} else {
checkbox = '<input type="checkbox"/>'
}
}
$('<li data-value="' + $(x).val() + '"><span class="handle">⣿</span>'+ checkbox + $(x).text() + '</li>').appendTo($ul);
});
$ul.appendTo(element);
function multisort_sync() {
var $select = $(element).find('select');
var options = Array();
$ul.find('li').each(function(i, x) {
var selected = true;
if (checkboxes && $(x).find('input[type=checkbox]:checked').length == 0) {
selected = false;
}
var value = $(x).data('value');
var $option = $select.find('[value="' + value + '"]');
if (selected) {
$option.prop('selected', 'selected');
} else {
$option.prop('selected', null);
}
$option.detach();
options.push($option);
});
while (options.length) {
$select.prepend(options.pop());
}
}
$ul.find('input[type=checkbox]').on('change', function() {
multisort_sync();
});
$ul.sortable({
handle: '.handle',
update: function(event, ui) {
var options = Array();
var select = $(element).find('select');
$ul.find('li').each(function(i, x) {
options.push($(element).find("option[value='" + $(x).data('value') + "']").attr('selected', 'selected').detach());
});
while (options.length) {
select.prepend(options.pop());
}
multisort_sync();
}
});
}

69
combo/utils/forms.py Normal file
View File

@ -0,0 +1,69 @@
# combo - content management system
# Copyright (C) 2014-2018 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import django
from django import forms
from django.utils.datastructures import MultiValueDict
from django.utils.safestring import mark_safe
class MultiSortWidget(forms.SelectMultiple):
def __init__(self, *args, **kwargs):
if 'with_checkboxes' in kwargs:
self.with_checkboxes = kwargs.pop('with_checkboxes')
else:
self.with_checkboxes = False
super(MultiSortWidget, self).__init__(*args, **kwargs)
def render(self, name, value, attrs=None, choices=()):
# reorder choices to get them in the current value order
self_choices = self.choices[:]
choices_dict = dict(self_choices)
if value:
for option in reversed(value.get('data')):
if option not in choices_dict:
continue
option_tuple = (option, choices_dict[option])
self.choices.remove(option_tuple)
self.choices.insert(0, option_tuple)
# render the <select multiple>
if django.VERSION < (1, 11, 0):
rendered = super(MultiSortWidget, self).render(name, value,
attrs=attrs, choices=choices)
else:
rendered = super(MultiSortWidget, self).render(name, value,
attrs=attrs)
# include it in a <div> that will be turned into an appropriate widget
# in javascript
id_ = 'wid-%s' % name
if self.with_checkboxes:
attrs = 'data-checkboxes="true"'
else:
attrs = ''
return mark_safe('''<div class="multisort" %s id="%s">%s</div>
<script type="text/javascript">multisort($("#%s"));</script>
''' % (attrs, id_, rendered, id_))
def render_options(self, choices, value):
value = value.get('data') or []
return super(MultiSortWidget, self).render_options(choices, value)
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
return {'data': data.getlist(name)}
return data.get(name, None)

View File

@ -1,6 +1,7 @@
import json
import os
import pytest
import re
import mock
from django.conf import settings
@ -15,6 +16,9 @@ from combo.apps.search.models import SearchCell
from combo.data.models import Page, JsonCell, TextCell, MenuCell, LinkCell
from combo.data.search_indexes import PageIndex
from .test_manager import admin_user, login
pytestmark = pytest.mark.django_db
@ -276,3 +280,42 @@ def test_update_index_command(app):
resp = app.get('/api/search/?q=bar', status=200)
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['url'] == 'http://example.net'
def test_manager_search_cell(app, admin_user):
Page.objects.all().delete()
page = Page(title='One', slug='one', template_name='standard')
page.save()
app = login(app)
resp = app.get('/manage/pages/%s/' % page.id)
resp = app.get(resp.html.find('option',
**{'data-add-url': re.compile('search_searchcell')})['data-add-url'])
cells = Page.objects.get(id=page.id).get_cells()
assert len(cells) == 1
assert isinstance(cells[0], SearchCell)
resp = app.get('/manage/pages/%s/' % page.id)
assert ('data-cell-reference="%s"' % cells[0].get_reference()) in resp.text
assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 1
with SearchServices(SEARCH_SERVICES):
resp = app.get('/manage/pages/%s/' % page.id)
assert len(resp.form['c%s-_search_services' % cells[0].get_reference()].options) == 3
# simulate reordering of options
resp.form['c%s-_search_services' % cells[0].get_reference()].options = [
(u'search_tmpl', False, u'Search with template'),
(u'_text', False, u'Page Contents'),
(u'search1', False, u'Search 1')]
resp.form['c%s-_search_services' % cells[0].get_reference()].value = ['search_tmpl', '_text']
resp = resp.form.submit()
assert resp.status_int == 302
# check selected engines are selected and the first items of the list
resp = app.get('/manage/pages/%s/' % page.id)
assert set(resp.form['c%s-_search_services' % cells[0].get_reference()].value) == set(['search_tmpl', '_text'])
assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[0][0] == 'search_tmpl'
assert resp.form['c%s-_search_services' % cells[0].get_reference()].options[1][0] == '_text'
# check there's no crash if search engines are removed from config
resp = app.get('/manage/pages/%s/' % page.id)
assert resp.form['c%s-_search_services' % cells[0].get_reference()].value == ['_text']