Repository: carltongibson/django-filter

Issue:
(2.0) FilterView always returning empty QuerySet for unbound FilterSet. 
I have the a view, `AccountList`, which is trying to render a django_table2 table. The view's source code:

    class AccountList(SingleTableMixin, FilterView):
        model = Account
        table_class = AccountTable
        template_name = 'accounts/account_list.html'
        context_table_name = 'object_list'
        ordering = ['vps']
    
        filterset_class = AccountFilter


This view is currently using this filterset (from django_filters): 


    import django_filters
    from accounts.models import Account

    class AccountFilter(django_filters.FilterSet):
        class Meta:
            model = Account
            fields = ['is_suspended', 'is_abandoned']
    
        is_suspended = django_filters.BooleanFilter(name='is_suspended', initial='False')
        is_abandoned = django_filters.BooleanFilter(name='is_abandoned', initial='False')
    
        def __init__(self, data=None, *args, **kwargs):
            # if filterset is bound, use initial values as defaults
            if data is not None:
                # get a mutable copy of the QueryDict
                data = data.copy()
    
                for name, f in self.base_filters.items():
                    initial = f.extra.get('initial')
    
                    # filter param is either missing or empty, use initial as default
                    if not data.get(name) and initial:
                        data[name] = initial
    
            super().__init__(data, *args, **kwargs)


Using this template:


    {% if filter %}
        <form action="" method="get" class="form form-inline">
            {{ filter.form.as_p }}
            <input type="submit" />
        </form>
    {% endif %}
    
    {% render_table object_list %}
    
    {% endblock %}


This is my from my urls.py

    path('', login_required(AccountList.as_view())),

When I visit my page, `127.0.0.1:8000`, I see that the filters are not set:
[![enter image description here][1]][1]

But then if i do `127.0.0.1:8000?page=1`, I see the filters are initialized properly:

[![enter image description here][2]][2]


  [1]: https://i.stack.imgur.com/nGmAz.png
  [2]: https://i.stack.imgur.com/PDXBB.png


## What is causing my filters to not have default value when I don't have page=1 appended to my url? 


Discussion:
Try using an actual `False` boolean value instead of the string value `'False'`.
So when I tried the following:

    is_suspended = django_filters.BooleanFilter(name='is_suspended', initial=False)
    is_abandoned = django_filters.BooleanFilter(name='is_abandoned', initial=False)

This is how the filters are rendered on `127.0.0.1:8000/`

![image](https://user-images.githubusercontent.com/28204629/42111943-f3164c04-7b9a-11e8-9644-ad78f3bd71b3.png)

Even though the filter was rendered with the right values, the filter had no effect as I can still see accounts that are suspended or abandoned. 

Moreover, when I visit page 2 by pressing the `2` button at the bottom, I get to `http://127.0.0.1:8000/?page=2` with the filters resetting to `Unknown`.

![image](https://user-images.githubusercontent.com/28204629/42111956-072e8a12-7b9b-11e8-9a3d-15e8473ab287.png)

Do you see any reason why? 
Sorry - nothing obvious comes to mind. 

For the first issue, I'd recommend looking at the resulting SQL query. Make sure the `suspended` and `abandoned` filters are actually being applied.
For the second issue, I'd check the incoming data. It's possible that an invalid value is being provided to the `suspended` and `abandoned` filters when changing pages. 
I inserted a few print statements and I found the following:

    def __init__(self, data=None, *args, **kwargs):
        if data is not None:        # 1
            data = data.copy()

            for name, f in self.base_filters.items():
                initial = f.extra.get('initial')   

                # filter param is either missing or empty, use initial as default
                if not data.get(name) and initial:
                    data[name] = initial

        super(BaseFilterSet, self).__init__(data, *args, **kwargs)

So it seems that `data` is `None`. Will you happen to know why my class based view is not passing `data` into `AccountFilter`? 


Hm. The `data` argument should always be provided by `FilterView`. 

https://github.com/carltongibson/django-filter/blob/b1f1c6592ef64a2172ec39040607a9c0b0714140/django_filters/views.py#L39-L46

Is `data` empty for both the first and second pages? 
This isn't addressable as is. Closing pending enough info to identify an issue. 
Same problem here after upgrade to 2.0. Either default page or empty filter should be present in the URL to get the results. Is this at least documented somewhere?
Still need more info to be able to reproduce...
`page`? Isn't this to do with your pagination, rather than Django Filter?
What info is needed exactly? Upgrading to 2.0 leads to the described behavior. Downgrading to 1.1.0 gets everything back to normal. I tried simplifying the view and the filterset to as minimalistic as possible, the behavior persists, so it seems like this should be fairly easy to reproduce.
@moorchegue, a minimal example test case or test project that demonstrates the issue would be helpful. 
It also looks like the issue is with whatever is providing your pagination logic. Yes, there may be some incompatibility with the new version, but debugging issues in other packages is out of scope. You need to demonstrate a bug in Django Filter for there to be anything we can do here. 
OK, so the problem here is with the `is_valid()` method introduced as part of #788. 

https://github.com/carltongibson/django-filter/blob/1dde11c70eedeac32f6de92c426deb289e1aebd8/django_filters/filterset.py#L202-L206

This automatically returns `False` when `data` is `None`. 

In the view we're then setting `object_list` to `qs.none()`.

https://github.com/carltongibson/django-filter/blob/1dde11c70eedeac32f6de92c426deb289e1aebd8/django_filters/views.py#L80-L83

Hence the observed behaviour. 

The [test for this](https://github.com/carltongibson/django-filter/blob/1dde11c70eedeac32f6de92c426deb289e1aebd8/tests/test_views.py#L28-L31) isn't failing because the test template is using `filter.qs` rather than `object_list`. 

https://github.com/carltongibson/django-filter/blob/1dde11c70eedeac32f6de92c426deb289e1aebd8/tests/templates/tests/book_filter.html#L3-L5

The immediate work around is to set `strict = False` on your filterset. This has consequence that actual invalid filter parameters will lead to partial filtering rather than showing empty results, but it should be workable for the now. (Better is to override the view `get` logic of course...) 

We'll have a think and improve the handling for the unbound case. 

Hm. The primary deficiency here is the disparity between the View's `object_list` and the FilterSet's `.qs`. Moving the strict behavior back to the `FilterSet.qs` would fix that. 

Additionally, there  is no strict/non-strict handling for the DRF backend.
I'll WIP this up.
@rpkilby Is there any progress on this issue?
Yep - started the PR but had to put my open source work on hold. I should be able to pick up where I left off fairly soon.
Note that the workaround should be to set `strict = False` on your `FilterMixin` descendant (probably the `View`.
Can't you just change the condition to return the qs in case of unbounded form?

e.g.

```
-        if self.filterset.is_valid() or not self.get_strict():
+        if not self.filterset.is_bound or self.filterset.is_valid() or not self.get_strict():
```

https://github.com/carltongibson/django-filter/pull/1007

Thanks.

Resolving patch:
diff --git a/django_filters/views.py b/django_filters/views.py
--- a/django_filters/views.py
+++ b/django_filters/views.py
@@ -77,7 +77,7 @@ def get(self, request, *args, **kwargs):
         filterset_class = self.get_filterset_class()
         self.filterset = self.get_filterset(filterset_class)
 
-        if self.filterset.is_valid() or not self.get_strict():
+        if not self.filterset.is_bound or self.filterset.is_valid() or not self.get_strict():
             self.object_list = self.filterset.qs
         else:
             self.object_list = self.filterset.queryset.none()


Test patch:
diff --git a/tests/templates/tests/book_filter.html b/tests/templates/tests/book_filter.html
--- a/tests/templates/tests/book_filter.html
+++ b/tests/templates/tests/book_filter.html
@@ -1,5 +1,5 @@
 {{ filter.form }}
 
-{% for obj in filter.qs %}
+{% for obj in object_list %}
     {{ obj }}
 {% endfor %}
diff --git a/tests/test_views.py b/tests/test_views.py
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -123,6 +123,19 @@ class View(FilterView):
         self.assertEqual(message, expected)
         self.assertEqual(len(recorded), 0)
 
+    def test_view_with_unbound_filter_form_returns_initial_queryset(self):
+        factory = RequestFactory()
+        request = factory.get(self.base_url)
+
+        queryset = Book.objects.filter(title='Snowcrash')
+        view = FilterView.as_view(model=Book, queryset=queryset)
+
+        response = view(request)
+        titles = [o.title for o in response.context_data['object_list']]
+
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(titles, ['Snowcrash'])
+
 
 class GenericFunctionalViewTests(GenericViewTestCase):
     base_url = '/books-legacy/'