Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(2.0) FilterView always returning empty QuerySet for unbound FilterSet. #930

Closed
vincentwhales opened this issue Jun 29, 2018 · 19 comments
Closed
Labels

Comments

@vincentwhales
Copy link

vincentwhales commented Jun 29, 2018

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

But then if i do 127.0.0.1:8000?page=1, I see the filters are initialized properly:

enter image description here

What is causing my filters to not have default value when I don't have page=1 appended to my url?

@rpkilby
Copy link
Collaborator

rpkilby commented Jun 29, 2018

Try using an actual False boolean value instead of the string value 'False'.

@vincentwhales
Copy link
Author

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

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

Do you see any reason why?

@rpkilby
Copy link
Collaborator

rpkilby commented Jun 29, 2018

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.

@vincentwhales
Copy link
Author

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?

@rpkilby
Copy link
Collaborator

rpkilby commented Jun 29, 2018

Hm. The data argument should always be provided by FilterView.

def get_filterset_kwargs(self, filterset_class):
"""
Returns the keyword arguments for instanciating the filterset.
"""
kwargs = {
'data': self.request.GET or None,
'request': self.request,
}

Is data empty for both the first and second pages?

@carltongibson
Copy link
Owner

This isn't addressable as is. Closing pending enough info to identify an issue.

@moorchegue
Copy link

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?

@carltongibson
Copy link
Owner

Still need more info to be able to reproduce...

@carltongibson
Copy link
Owner

page? Isn't this to do with your pagination, rather than Django Filter?

@moorchegue
Copy link

moorchegue commented Jul 28, 2018

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.

@rpkilby
Copy link
Collaborator

rpkilby commented Jul 28, 2018

@moorchegue, a minimal example test case or test project that demonstrates the issue would be helpful.

@carltongibson
Copy link
Owner

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.

@carltongibson
Copy link
Owner

carltongibson commented Aug 3, 2018

OK, so the problem here is with the is_valid() method introduced as part of #788.

def is_valid(self):
"""
Return True if the underlying form has no errors, or False otherwise.
"""
return self.is_bound and self.form.is_valid()

This automatically returns False when data is None.

In the view we're then setting object_list to qs.none().

if self.filterset.is_valid() or not self.get_strict():
self.object_list = self.filterset.qs
else:
self.object_list = self.filterset.queryset.none()

Hence the observed behaviour.

The test for this isn't failing because the test template is using filter.qs rather than object_list.

{% for obj in filter.qs %}
{{ obj }}
{% endfor %}

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.

@carltongibson carltongibson reopened this Aug 3, 2018
@carltongibson carltongibson changed the title Initial value only present when ?page=1 is appended to url? (2.0) FilterView always returning empty QuerySet for unbound FilterSet. Aug 3, 2018
@rpkilby
Copy link
Collaborator

rpkilby commented Aug 3, 2018

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.

@rpkilby
Copy link
Collaborator

rpkilby commented Aug 3, 2018

I'll WIP this up.

@jieter
Copy link
Contributor

jieter commented Sep 4, 2018

@rpkilby Is there any progress on this issue?

@rpkilby
Copy link
Collaborator

rpkilby commented Sep 4, 2018

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.

@dyve
Copy link

dyve commented Oct 2, 2018

Note that the workaround should be to set strict = False on your FilterMixin descendant (probably the View.

@ekeydar
Copy link

ekeydar commented Nov 17, 2018

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():

#1007

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants