search: add custom widget to sort and enable engines (#23534)
This commit is contained in:
parent
3328fccc06
commit
9c69420972
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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']
|
||||
|
|
Loading…
Reference in New Issue