diff --git a/django_select2/forms.py b/django_select2/forms.py index 132d53a..a072c03 100644 --- a/django_select2/forms.py +++ b/django_select2/forms.py @@ -70,16 +70,18 @@ class Select2Mixin: form media. """ - def build_attrs(self, *args, **kwargs): + def build_attrs(self, base_attrs, extra_attrs=None): """Add select2 data attributes.""" - attrs = super(Select2Mixin, self).build_attrs(*args, **kwargs) + default_attrs = {'data-minimum-input-length': 0} if self.is_required: - attrs.setdefault('data-allow-clear', 'false') + default_attrs['data-allow-clear'] = 'false' else: - attrs.setdefault('data-allow-clear', 'true') - attrs.setdefault('data-placeholder', '') + default_attrs['data-allow-clear'] = 'true' + default_attrs['data-placeholder'] = '' + + default_attrs.update(base_attrs) + attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs) - attrs.setdefault('data-minimum-input-length', 0) if 'class' in attrs: attrs['class'] += ' django-select2' else: @@ -120,12 +122,15 @@ class Select2Mixin: class Select2TagMixin: """Mixin to add select2 tag functionality.""" - def build_attrs(self, *args, **kwargs): + def build_attrs(self, base_attrs, extra_attrs=None): """Add select2's tag attributes.""" - self.attrs.setdefault('data-minimum-input-length', 1) - self.attrs.setdefault('data-tags', 'true') - self.attrs.setdefault('data-token-separators', '[",", " "]') - return super(Select2TagMixin, self).build_attrs(*args, **kwargs) + default_attrs = { + 'data-minimum-input-length': 1, + 'data-tags': 'true', + 'data-token-separators': '[",", " "]' + } + default_attrs.update(base_attrs) + return super().build_attrs(default_attrs, extra_attrs=extra_attrs) class Select2Widget(Select2Mixin, forms.Select): @@ -226,20 +231,27 @@ class HeavySelect2Mixin: return self.data_url return reverse(self.data_view) - def build_attrs(self, *args, **kwargs): + def build_attrs(self, base_attrs, extra_attrs=None): """Set select2's AJAX attributes.""" - attrs = super(HeavySelect2Mixin, self).build_attrs(*args, **kwargs) + + default_attrs = { + 'data-ajax--url': self.get_url(), + 'data-ajax--cache': "true", + 'data-ajax--type': "GET", + 'data-minimum-input-length': 2, + } + + if self.dependent_fields: + default_attrs['data-select2-dependent-fields'] = " ".join(self.dependent_fields) + + default_attrs.update(base_attrs) + + attrs = super().build_attrs(default_attrs, extra_attrs=extra_attrs) # encrypt instance Id self.widget_id = signing.dumps(id(self)) attrs['data-field_id'] = self.widget_id - attrs.setdefault('data-ajax--url', self.get_url()) - attrs.setdefault('data-ajax--cache', "true") - attrs.setdefault('data-ajax--type', "GET") - attrs.setdefault('data-minimum-input-length', 2) - if self.dependent_fields: - attrs.setdefault('data-select2-dependent-fields', " ".join(self.dependent_fields)) attrs['class'] += ' django-select2-heavy' return attrs diff --git a/tests/conftest.py b/tests/conftest.py index 265318b..6c46557 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import random import string import pytest -from django.utils.encoding import force_text from selenium import webdriver from selenium.common.exceptions import WebDriverException @@ -22,11 +21,11 @@ def random_name(n): @pytest.yield_fixture(scope='session') def driver(): chrome_options = webdriver.ChromeOptions() - chrome_options.headless = True + chrome_options.headless = False try: b = webdriver.Chrome(options=chrome_options) except WebDriverException as e: - pytest.skip(force_text(e)) + pytest.skip(str(e)) else: yield b b.quit() diff --git a/tests/test_forms.py b/tests/test_forms.py index 92ff22f..53e8d34 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -217,12 +217,17 @@ class TestHeavySelect2Mixin(TestSelect2Mixin): driver.find_element_by_css_selector('.select2-results') elem1, elem2 = driver.find_elements_by_css_selector('.select2-selection') - elem1.click() + elem1.click() + search1 = driver.find_element_by_css_selector('.select2-search__field') + search1.send_keys('fo') result1 = WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child')) ).text + elem2.click() + search2 = driver.find_element_by_css_selector('.select2-search__field') + search2.send_keys('fo') result2 = WebDriverWait(driver, 60).until( expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.select2-results li:first-child')) ).text @@ -315,6 +320,37 @@ class TestModelSelect2Mixin(TestHeavySelect2Mixin): widget.queryset = Genre.objects.all() assert isinstance(widget.get_queryset(), QuerySet) + def test_tag_attrs_Select2Widget(self): + widget = Select2Widget() + output = widget.render('name', 'value') + assert 'data-minimum-input-length="0"' in output + + def test_custom_tag_attrs_Select2Widget(self): + widget = Select2Widget(attrs={'data-minimum-input-length': '3'}) + output = widget.render('name', 'value') + assert 'data-minimum-input-length="3"' in output + + def test_tag_attrs_ModelSelect2Widget(self): + widget = ModelSelect2Widget(queryset=Genre.objects.all(), search_fields=['title__icontains']) + output = widget.render('name', 'value') + assert 'data-minimum-input-length="2"' in output + + def test_tag_attrs_ModelSelect2TagWidget(self): + widget = ModelSelect2TagWidget(queryset=Genre.objects.all(), search_fields=['title__icontains']) + output = widget.render('name', 'value') + assert 'data-minimum-input-length="2"' in output + + def test_tag_attrs_HeavySelect2Widget(self): + widget = HeavySelect2Widget(data_url='/foo/bar/') + output = widget.render('name', 'value') + assert 'data-minimum-input-length="2"' in output + + def test_custom_tag_attrs_ModelSelect2Widget(self): + widget = ModelSelect2Widget( + queryset=Genre.objects.all(), search_fields=['title__icontains'], attrs={'data-minimum-input-length': '3'}) + output = widget.render('name', 'value') + assert 'data-minimum-input-length="3"' in output + def test_get_search_fields(self): widget = ModelSelect2Widget() with pytest.raises(NotImplementedError): @@ -382,7 +418,7 @@ class TestHeavySelect2TagWidget(TestHeavySelect2Mixin): def test_tag_attrs(self): widget = ModelSelect2TagWidget(queryset=Genre.objects.all(), search_fields=['title__icontains']) output = widget.render('name', 'value') - assert 'data-minimum-input-length="1"' in output + assert 'data-minimum-input-length="2"' in output assert 'data-tags="true"' in output assert 'data-token-separators' in output diff --git a/tests/testapp/forms.py b/tests/testapp/forms.py index 4f12fec..e858b7f 100644 --- a/tests/testapp/forms.py +++ b/tests/testapp/forms.py @@ -149,11 +149,19 @@ class HeavySelect2WidgetForm(forms.Form): class HeavySelect2MultipleWidgetForm(forms.Form): title = forms.CharField(max_length=50) genres = forms.MultipleChoiceField( - widget=HeavySelect2MultipleWidget(data_view='heavy_data_1', choices=NUMBER_CHOICES), + widget=HeavySelect2MultipleWidget( + data_view='heavy_data_1', + choices=NUMBER_CHOICES, + attrs={'data-minimum-input-length': 0}, + ), choices=NUMBER_CHOICES ) featured_artists = forms.MultipleChoiceField( - widget=HeavySelect2MultipleWidget(data_view='heavy_data_2', choices=NUMBER_CHOICES), + widget=HeavySelect2MultipleWidget( + data_view='heavy_data_2', + choices=NUMBER_CHOICES, + attrs={'data-minimum-input-length': 0}, + ), choices=NUMBER_CHOICES, required=False ) @@ -182,6 +190,7 @@ class AddressChainedSelect2WidgetForm(forms.Form): search_fields=['name__icontains'], max_results=500, dependent_fields={'city': 'cities'}, + attrs={'data-minimum-input-length': 0}, ) ) @@ -193,6 +202,7 @@ class AddressChainedSelect2WidgetForm(forms.Form): search_fields=['name__icontains'], dependent_fields={'country': 'country'}, max_results=500, + attrs={'data-minimum-input-length': 0}, ) )