diff --git a/src/dal_select2/widgets.py b/src/dal_select2/widgets.py index 186e3e48..0f78b943 100644 --- a/src/dal_select2/widgets.py +++ b/src/dal_select2/widgets.py @@ -1,4 +1,5 @@ """Select2 widget implementation module.""" +import ast try: from functools import lru_cache @@ -113,15 +114,55 @@ def media(self): autocomplete_function = 'select2' +class Select2InitialRenderMixin: + def render(self, name, value, attrs=None, renderer=None): + if not value: + return super().render(name, value, attrs=attrs, renderer=renderer) + + values = [] + + if isinstance(value, str): + try: + parsed = ast.literal_eval(value) + if isinstance(parsed, (list, tuple)): + values = [v.strip() for v in parsed if v] + else: + values = [str(parsed)] + except (ValueError, SyntaxError): + values = [v.strip() for v in value.split(',') if v.strip()] + + elif isinstance(value, (list, tuple)): + values = list(value) + + else: + values = [value] + + existing = dict(self.choices) + extended = [(v, v) for v in values if v not in existing] + if extended: + original_choices = self.choices + self.choices = list(self.choices) + extended + try: + return super().render(name, values, attrs=attrs, renderer=renderer) + finally: + self.choices = original_choices + + return super().render(name, values, attrs=attrs, renderer=renderer) + + class Select2(Select2WidgetMixin, Select): """Select2 widget for regular choices.""" -class Select2Multiple(Select2WidgetMixin, SelectMultiple): +class Select2Multiple(Select2InitialRenderMixin, Select2WidgetMixin, SelectMultiple): """Select2Multiple widget for regular choices.""" -class ListSelect2(WidgetMixin, Select2WidgetMixin, forms.Select): +class ListSelect2( + Select2InitialRenderMixin, + WidgetMixin, Select2WidgetMixin, + forms.Select +): """Select widget for regular choices and Select2.""" diff --git a/test_project/tests/test_widgets.py b/test_project/tests/test_widgets.py index 2ef3fec7..ca62046d 100644 --- a/test_project/tests/test_widgets.py +++ b/test_project/tests/test_widgets.py @@ -9,7 +9,10 @@ from django import forms from django import http from django import test +from django.http import HttpResponse from django.urls import re_path as url +from django.views import View + try: from django.urls import reverse except ImportError: @@ -19,10 +22,15 @@ import mock +class DummyView(View): + def get(self, request): + return HttpResponse("ok") + + urlpatterns = [ url( r'^test-url/$', - mock.Mock(), + DummyView.as_view(), # replace with sample Mock View name='test_url' ), ] @@ -158,3 +166,45 @@ def render_forward_conf(self, id): # attrs with id observed = widget.render('myname', '', attrs={'id': 'myid'}) self.assertEqual(observed, 'myid') + + +@override_settings(ROOT_URLCONF='tests.test_widgets') +class Select2InitialRenderMixinTest(test.TestCase): + def test_listselect2_adds_missing_selected_value(self): + class Form(forms.Form): + test = forms.ChoiceField( + widget=select2_widget.ListSelect2(url=reverse('test_url')), + required=False, + ) + + form = Form(initial={'test': 'Urdu'}) + rendered = form.as_p() + + assert 'value="Urdu" selected' in rendered + + def test_select2multiple_adds_multiple_selected_values(self): + class Form(forms.Form): + test = forms.MultipleChoiceField( + widget=select2_widget.Select2Multiple(url=reverse('test_url')), + required=False, + ) + + form = Form(initial={'test': ['English', 'Urdu']}) + rendered = form.as_p() + + assert 'value="English" selected' in rendered + assert 'value="Urdu" selected' in rendered + + def test_select2multiple_handles_stringified_list(self): + class Form(forms.Form): + test = forms.MultipleChoiceField( + widget=select2_widget.Select2Multiple(url=reverse('test_url')), + required=False, + ) + + form = Form() + form.fields['test'].initial = "['English', 'Urdu']" + rendered = form.as_p() + + assert 'value="English" selected' in rendered + assert 'value="Urdu" selected' in rendered