summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFrédéric Péters <fpeters@entrouvert.com>2018-08-11 08:35:49 (GMT)
committerThomas NOEL <tnoel@entrouvert.com>2018-08-30 09:50:06 (GMT)
commit9c69420972424f27e4b85a5d4c4f3f3a26778f99 (patch)
tree6b655183c0bf43093e75cd92d061dc071831c230
parent3328fccc060f4f1d1a381a26c2209218bf95f53d (diff)
downloadcombo-9c69420972424f27e4b85a5d4c4f3f3a26778f99.zip
combo-9c69420972424f27e4b85a5d4c4f3f3a26778f99.tar.gz
combo-9c69420972424f27e4b85a5d4c4f3f3a26778f99.tar.bz2
search: add custom widget to sort and enable engines (#23534)
-rw-r--r--combo/apps/search/engines.py8
-rw-r--r--combo/apps/search/forms.py34
-rw-r--r--combo/apps/search/models.py4
-rw-r--r--combo/apps/wcs/forms.py42
-rw-r--r--combo/manager/static/js/combo.manager.js48
-rw-r--r--combo/utils/forms.py69
-rw-r--r--tests/test_search.py43
7 files changed, 200 insertions, 48 deletions
diff --git a/combo/apps/search/engines.py b/combo/apps/search/engines.py
index 282b5fb..7b07290 100644
--- a/combo/apps/search/engines.py
+++ b/combo/apps/search/engines.py
@@ -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
diff --git a/combo/apps/search/forms.py b/combo/apps/search/forms.py
new file mode 100644
index 0000000..5bc5f4b
--- /dev/null
+++ b/combo/apps/search/forms.py
@@ -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)
diff --git a/combo/apps/search/models.py b/combo/apps/search/models.py
index 088422b..97a241c 100644
--- a/combo/apps/search/models.py
+++ b/combo/apps/search/models.py
@@ -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:
diff --git a/combo/apps/wcs/forms.py b/combo/apps/wcs/forms.py
index c8ea33b..1aee714 100644
--- a/combo/apps/wcs/forms.py
+++ b/combo/apps/wcs/forms.py
@@ -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
diff --git a/combo/manager/static/js/combo.manager.js b/combo/manager/static/js/combo.manager.js
index 498f86a..d2b2ba4 100644
--- a/combo/manager/static/js/combo.manager.js
+++ b/combo/manager/static/js/combo.manager.js
@@ -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();
}
});
}
diff --git a/combo/utils/forms.py b/combo/utils/forms.py
new file mode 100644
index 0000000..681da8e
--- /dev/null
+++ b/combo/utils/forms.py
@@ -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)
diff --git a/tests/test_search.py b/tests/test_search.py
index 8eed48c..358b146 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -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']