Repository: carltongibson/django-filter

Issue:
QueryArrayWidget mutates passed data
I think I've faced a small issue with the `QueryArrayWidget`, especially with this line: https://github.com/carltongibson/django-filter/blob/master/django_filters/widgets.py#L242, as described below:

```python
class QueryArrayWidget(BaseCSVWidget, forms.TextInput):

    def value_from_datadict(self, data, files, name):
        if not isinstance(data, MultiValueDict):
            for key, value in data.items():
                # treat value as csv string: ?foo=1,2
                if isinstance(value, str):
                    data[key] = [x.strip() for x in value.rstrip(',').split(',') if x]
```

The last line of this snippet will mutate the initial QueryDict, which can have unforeseen consequences. An easy fix could be to simply create a new dict and I can provide a PR with that if we agree on a fix :)

Edit: temporary workaround in my project is to subclass like that:

```python
class QueryArrayWidget(widgets.QueryArrayWidget):
    """
    Until https://github.com/carltongibson/django-filter/issues/1047 is fixed
    """

    def value_from_datadict(self, data, files, name):
        data = data.copy()
        return super().value_from_datadict(data, files, name)
```


Discussion:
OK, quickest possible scan, but yes, that looks right. `data` here is `request.GET` yes?
Yes, that's basically what's passed when instanciating a django form, so typically `request.GET` or `request.POST` if I understand correctly :)
OK. I can't quite swing my head round instantly to thinking what happens if you try and mutate those, but, regardless, it doesn't sound like a good idea, so yes PR sounds good. 

Thanks. 👍 
I'm confused by the `if not isinstance(data, MultiValueDict):` line. `request.GET`/`request.POST` are `QueryDict`s, which inherit `MultiValueDict`. When would this condition ever be triggered?
> OK. I can't quite swing my head round instantly to thinking what happens if you try and mutate those, but, regardless, it doesn't sound like a good idea, so yes PR sounds good.

basically, if you're using this widget in a form (or filterset) and this method is called before other widget's `value_from_datadict`, they will end up with lists of values instead of simple values to clean, which cause all kinds of unexpected crashes.

I'll submit a PR then :)
@carltongibson can we help on this one with a merge request or something?
@AMontagu Yes, thanks. 
@carltongibson someone in my team took it and made a PR.
@AMontagu Great thanks. It's in my queue! 🎁

Resolving patch:
diff --git a/django_filters/widgets.py b/django_filters/widgets.py
--- a/django_filters/widgets.py
+++ b/django_filters/widgets.py
@@ -246,6 +246,7 @@ class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
 
     def value_from_datadict(self, data, files, name):
         if not isinstance(data, MultiValueDict):
+            data = data.copy()
             for key, value in data.items():
                 # treat value as csv string: ?foo=1,2
                 if isinstance(value, str):


Test patch:
diff --git a/tests/test_filtering.py b/tests/test_filtering.py
--- a/tests/test_filtering.py
+++ b/tests/test_filtering.py
@@ -14,6 +14,7 @@
 from django_filters.filters import (
     AllValuesFilter,
     AllValuesMultipleFilter,
+    BaseInFilter,
     CharFilter,
     ChoiceFilter,
     DateFromToRangeFilter,
@@ -31,6 +32,7 @@
     TypedMultipleChoiceFilter,
 )
 from django_filters.filterset import FilterSet
+from django_filters.widgets import QueryArrayWidget
 
 from .models import (
     STATUS_CHOICES,
@@ -1977,10 +1979,10 @@ class Meta:
 
 class MiscFilterSetTests(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)
+        User.objects.create(username="alex", last_name="johnson", status=1)
+        User.objects.create(username="jacob", last_name="johnson", status=2)
+        User.objects.create(username="aaron", last_name="white", status=2)
+        User.objects.create(username="carl", last_name="black", status=0)
 
     def test_filtering_with_declared_filters(self):
         class F(FilterSet):
@@ -2056,3 +2058,36 @@ class Meta:
         f = F({"status": "2"}, queryset=qs)
         self.assertEqual(len(f.qs), 2)
         self.assertEqual(f.qs.count(), 2)
+
+    def test_filtering_with_widgets(self):
+        class CharInFilter(BaseInFilter, CharFilter):
+            pass
+
+        class F(FilterSet):
+            last_name = CharInFilter(widget=QueryArrayWidget)
+            username = CharInFilter()
+
+            class Meta:
+                model = User
+                fields = ["last_name", "username"]
+
+        qs = User.objects.all()
+
+        f = F({"last_name": ["johnson"]}, queryset=qs)
+        self.assertQuerysetEqual(
+            f.qs, ["alex", "jacob"], lambda o: o.username, ordered=False
+        )
+
+        f = F({"last_name": ["johnson"], "username": "carl"}, queryset=qs)
+        self.assertQuerysetEqual(f.qs, [], lambda o: o.username, ordered=False)
+
+        f = F({"last_name": ["johnson"], "username": "jacob"}, queryset=qs)
+        self.assertQuerysetEqual(f.qs, ["jacob"], lambda o: o.username, ordered=False)
+
+        f = F(
+            {"last_name": ["johnson", "white"], "username": "jacob, carl, aaron"},
+            queryset=qs,
+        )
+        self.assertQuerysetEqual(
+            f.qs, ["jacob", "aaron"], lambda o: o.username, ordered=False
+        )