Repository: carltongibson/django-filter

Issue:
OrderingFilter with widget=forms.Select() not working
In 0.15.0, when I try to set the widget for `OrderingFilter`, then such filter does not work anymore. For instance:

```
    o = OrderingFilter(
        widget=forms.Select(),
        # tuple-mapping retains order
        fields=(
            ('created', 'Created'),
        ),
        # labels do not need to retain order
        field_labels={
            'created': 'Created',
        }
    )
```

The ordering field now is rendered as a select (I am using crispy-forms) but, instead of ordering, the filter returns no results and as field error it says `c is not a valid choice`. 

Bugfix/496 ordering filter select widget
Ref #496 

Not to merge. Just showing the issue. 

We require: 
1. a Field using a CSVSelect widget. 
2. Docs saying use "either the default of CSVWidget" for this filter. 



Discussion:
I don't think you need to specify the widget here: 

`OrderingFilter` extends `ChoiceFilter` which uses Django's `ChoiceField`, which has a `Select` widget by default. 

You're also passing a widget instance, without the `choices` set, see the `Select()`, where you need to pass the class, i.e. just `Select`. 

I'm going to close this as it doesn't look like an issue. (Happy to re-open if there's more.)

I checked it out and the default widget seems to be "CSVWidget" not "Select"

OK. Fine. Can you add the default HTML that's generated for me? I'll verify what's going on. 

```
<div id="div_id_o" class="form-group row"> 
    <label for="id_o" class="col-form-label col-lg-3"> Ordering</label> 
     <div class="col-lg-9"> 
         <input class="csvwidget form-control" id="id_o" name="o" type="text"> 
     </div> 
</div>
```

This is what crispy-forms render in HTML. 

OK. And one more thing... what happens if you pass the `Select` class rather than an instance? 

```
...
        widget=forms.Select,  # No `()`
```

I had a similar problem. The displayed input form field for the OrderingField was set to a Text field. Setting `widget=forms.Select` for the OrderingField solved the issue.

It render correctly as a Select but, the filter does not work. It gives me a field validation error:
`Select a valid choice. C is not one of the available choices.`
Seems like it is only getting the first letter of the field value i.e. "Created"

You are right, same problem here.

Here's the rendered HTML:

```
<select class="select form-control form-control-danger" id="id_o" name="o">
     <option value="Created" selected="selected">Created</option>
     <option value="-Created">Created (descending)</option>
</select>
```

> Seems like it is only getting the first letter of the field value i.e. "Created"

That'll be CSVWidget doing it's thing...

`FilterClass().filters['o'].field.widget` returns:
`<django_filters.widgets.CSVWidget object at 0x7f175250dc50>`

The problem may be here:

```
class BaseCSVFilter(Filter):
    """
    Base class for CSV type filters, such as IN and RANGE.
    """
    base_field_class = BaseCSVField

    def __init__(self, *args, **kwargs):
        super(BaseCSVFilter, self).__init__(*args, **kwargs)

        class ConcreteCSVField(self.base_field_class, self.field_class):
            pass
        ConcreteCSVField.__name__ = self._field_class_name(
            self.field_class, self.lookup_expr
        )

        self.field_class = ConcreteCSVField
```

I think that the call to `super` sets your widget but then `self.field_class = ConcreteCSVField` messes all up.

https://github.com/carltongibson/django-filter/pull/499 should give you a workaround. 

The final fix won't look like that — it's not the right fix, but it should get you going. 

DO feedback if you try it and hit issues!



Resolving patch:
diff --git a/django_filters/fields.py b/django_filters/fields.py
--- a/django_filters/fields.py
+++ b/django_filters/fields.py
@@ -11,7 +11,7 @@
 from django.utils.translation import ugettext_lazy as _
 
 from .utils import handle_timezone
-from .widgets import RangeWidget, LookupTypeWidget, CSVWidget
+from .widgets import RangeWidget, LookupTypeWidget, CSVWidget, BaseCSVWidget
 
 
 class RangeField(forms.MultiValueField):
@@ -129,7 +129,28 @@ class IntegerCSVField(BaseCSVField, filters.IntegerField):
             pass
 
     """
-    widget = CSVWidget
+    base_widget_class = BaseCSVWidget
+
+    def __init__(self, *args, **kwargs):
+        widget = kwargs.get('widget') or self.widget
+        kwargs['widget'] = self._get_widget_class(widget)
+
+        super(BaseCSVField, self).__init__(*args, **kwargs)
+
+    def _get_widget_class(self, widget):
+        # passthrough, allows for override
+        if isinstance(widget, BaseCSVWidget) or (
+                isinstance(widget, type) and
+                issubclass(widget, BaseCSVWidget)):
+            return widget
+
+        # complain since we are unable to reconstruct widget instances
+        assert isinstance(widget, type), \
+            "'%s.widget' must be a widget class, not %s." \
+            % (self.__class__.__name__, repr(widget))
+
+        bases = (self.base_widget_class, widget, )
+        return type(str('CSV%s' % widget.__name__), bases, {})
 
     def clean(self, value):
         if value is None:
@@ -138,6 +159,10 @@ def clean(self, value):
 
 
 class BaseRangeField(BaseCSVField):
+    # Force use of text input, as range must always have two inputs. A date
+    # input would only allow a user to input one value and would always fail.
+    widget = CSVWidget
+
     default_error_messages = {
         'invalid_values': _('Range query expects two values.')
     }
diff --git a/django_filters/widgets.py b/django_filters/widgets.py
--- a/django_filters/widgets.py
+++ b/django_filters/widgets.py
@@ -138,12 +138,12 @@ def value_from_datadict(self, data, files, name):
         }.get(value, None)
 
 
-class CSVWidget(forms.TextInput):
+class BaseCSVWidget(forms.Widget):
     def _isiterable(self, value):
         return isinstance(value, Iterable) and not isinstance(value, string_types)
 
     def value_from_datadict(self, data, files, name):
-        value = super(CSVWidget, self).value_from_datadict(data, files, name)
+        value = super(BaseCSVWidget, self).value_from_datadict(data, files, name)
 
         if value is not None:
             if value == '':  # empty value should parse as an empty list
@@ -152,8 +152,22 @@ def value_from_datadict(self, data, files, name):
         return None
 
     def render(self, name, value, attrs=None):
-        if self._isiterable(value):
-            value = [force_text(format_value(self, v)) for v in value]
-            value = ','.join(list(value))
+        if not self._isiterable(value):
+            value = [value]
 
-        return super(CSVWidget, self).render(name, value, attrs)
+        if len(value) <= 1:
+            # delegate to main widget (Select, etc...) if not multiple values
+            value = value[0] if value else value
+            return super(BaseCSVWidget, self).render(name, value, attrs)
+
+        # if we have multiple values, we need to force render as a text input
+        # (otherwise, the additional values are lost)
+        surrogate = forms.TextInput()
+        value = [force_text(format_value(surrogate, v)) for v in value]
+        value = ','.join(list(value))
+
+        return surrogate.render(name, value, attrs)
+
+
+class CSVWidget(BaseCSVWidget, forms.TextInput):
+    pass


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -3,14 +3,12 @@
 
 from datetime import datetime, time, timedelta, tzinfo
 import decimal
-import unittest
 
-import django
 from django import forms
 from django.test import TestCase, override_settings
 from django.utils.timezone import make_aware, get_default_timezone
 
-from django_filters.widgets import RangeWidget
+from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
 from django_filters.fields import (
     Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
     DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
@@ -196,6 +194,25 @@ def test_validation_error(self):
         with self.assertRaises(forms.ValidationError):
             self.field.clean(['a', 'b', 'c'])
 
+    def test_derived_widget(self):
+        with self.assertRaises(AssertionError) as excinfo:
+            BaseCSVField(widget=RangeWidget())
+
+        msg = str(excinfo.exception)
+        self.assertIn("'BaseCSVField.widget' must be a widget class", msg)
+        self.assertIn("RangeWidget", msg)
+
+        widget = CSVWidget()
+        field = BaseCSVField(widget=widget)
+        self.assertIs(field.widget, widget)
+
+        field = BaseCSVField(widget=CSVWidget)
+        self.assertIsInstance(field.widget, CSVWidget)
+
+        field = BaseCSVField(widget=forms.Select)
+        self.assertIsInstance(field.widget, forms.Select)
+        self.assertIsInstance(field.widget, BaseCSVWidget)
+
 
 class BaseRangeFieldTests(TestCase):
     def setUp(self):
diff --git a/tests/test_filtering.py b/tests/test_filtering.py
--- a/tests/test_filtering.py
+++ b/tests/test_filtering.py
@@ -6,6 +6,7 @@
 import unittest
 
 import django
+from django import forms
 from django.test import TestCase, override_settings
 from django.utils import six
 from django.utils.timezone import now
@@ -24,6 +25,7 @@
 from django_filters.filters import MultipleChoiceFilter
 from django_filters.filters import ModelMultipleChoiceFilter
 from django_filters.filters import NumberFilter
+from django_filters.filters import OrderingFilter
 from django_filters.filters import RangeFilter
 from django_filters.filters import TimeRangeFilter
 # from django_filters.widgets import LinkWidget
@@ -1586,6 +1588,46 @@ def test_related_filtering(self):
         self.assertEqual(f.qs.count(), 2)
 
 
+class OrderingFilterTests(TestCase):
+
+    def setUp(self):
+        User.objects.create(username='alex', status=1)
+        User.objects.create(username='jacob', status=2)
+        User.objects.create(username='aaron', status=2)
+        User.objects.create(username='carl', status=0)
+
+    def test_ordering(self):
+        class F(FilterSet):
+            o = OrderingFilter(
+                fields=('username', )
+            )
+
+            class Meta:
+                model = User
+                fields = ['username']
+
+        qs = User.objects.all()
+        f = F({'o': 'username'}, queryset=qs)
+        names = f.qs.values_list('username', flat=True)
+        self.assertEqual(list(names), ['aaron', 'alex', 'carl', 'jacob'])
+
+    def test_ordering_with_select_widget(self):
+        class F(FilterSet):
+            o = OrderingFilter(
+                widget=forms.Select,
+                fields=('username', )
+            )
+
+            class Meta:
+                model = User
+                fields = ['username']
+
+        qs = User.objects.all()
+        f = F({'o': 'username'}, queryset=qs)
+        names = f.qs.values_list('username', flat=True)
+        self.assertEqual(list(names), ['aaron', 'alex', 'carl', 'jacob'])
+
+
 class MiscFilterSetTests(TestCase):
 
     def setUp(self):
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -9,7 +9,7 @@
 from django import forms
 from django.test import TestCase, override_settings
 
-from django_filters import filters
+from django_filters import filters, widgets
 from django_filters.fields import (
     Lookup,
     RangeField,
@@ -1097,3 +1097,10 @@ def test_normalize_fields(self):
         with self.assertRaises(AssertionError) as ctx:
             f([0, 1, 2])
         self.assertEqual(str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.")
+
+    def test_widget(self):
+        f = OrderingFilter()
+        widget = f.field.widget
+
+        self.assertIsInstance(widget, widgets.BaseCSVWidget)
+        self.assertIsInstance(widget, forms.Select)
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -5,6 +5,7 @@
 from django.forms import TextInput, Select
 
 from django_filters.widgets import BooleanWidget
+from django_filters.widgets import BaseCSVWidget
 from django_filters.widgets import CSVWidget
 from django_filters.widgets import RangeWidget
 from django_filters.widgets import LinkWidget
@@ -182,6 +183,9 @@ def test_widget(self):
         self.assertHTMLEqual(w.render('price', ''), """
             <input type="text" name="price" />""")
 
+        self.assertHTMLEqual(w.render('price', '1'), """
+            <input type="text" name="price" value="1" />""")
+
         self.assertHTMLEqual(w.render('price', '1,2'), """
             <input type="text" name="price" value="1,2" />""")
 
@@ -224,3 +228,54 @@ def test_widget_value_from_datadict(self):
 
         result = w.value_from_datadict({}, {}, 'price')
         self.assertEqual(result, None)
+
+
+class CSVSelectTests(TestCase):
+    class CSVSelect(BaseCSVWidget, Select):
+        pass
+
+    def test_widget(self):
+        w = self.CSVSelect(choices=((1, 'a'), (2, 'b')))
+        self.assertHTMLEqual(
+            w.render('price', None),
+            """
+            <select name="price">
+                <option value="1">a</option>
+                <option value="2">b</option>
+            </select>
+            """
+        )
+
+        self.assertHTMLEqual(
+            w.render('price', ''),
+            """
+            <select name="price">
+                <option value="1">a</option>
+                <option value="2">b</option>
+            </select>
+            """)
+
+        self.assertHTMLEqual(
+            w.render('price', '1'),
+            """
+            <select name="price">
+                <option selected="selected" value="1">a</option>
+                <option value="2">b</option>
+            </select>
+            """)
+
+        self.assertHTMLEqual(
+            w.render('price', '1,2'),
+            """
+            <select name="price">
+                <option value="1">a</option>
+                <option value="2">b</option>
+            </select>
+            """
+        )
+
+        self.assertHTMLEqual(w.render('price', ['1', '2']), """
+            <input type="text" name="price" value="1,2" />""")
+
+        self.assertHTMLEqual(w.render('price', [1, 2]), """
+            <input type="text" name="price" value="1,2" />""")