简体   繁体   中英

Django-filter: ModelChoiceFilter with request.user based queryset

I have a Django class based ListView listing objects. These objects can be filtered based on locations. Now I want that the location ModelChoiceFilter only lists locations which are relevant to the current user. Relevant locations are the locations he owns. How can I change the queryset?

# models.py
from django.db import models
from django.conf import settings
from rules.contrib.models import RulesModel
from django.utils.translation import gettext_lazy as _


class Location(RulesModel):
    name = models.CharField(_("Name"), max_length=200)
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_("Owner"),
        related_name="location_owner",
        on_delete=models.CASCADE,
        help_text=_("Owner can view, change or delete this location."),
    )

class Object(RulesModel):
    name = models.CharField(_("Name"), max_length=200)
    description = models.TextField(_("Description"), blank=True)
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        verbose_name=_("Owner"),
        related_name="location_owner",
        on_delete=models.CASCADE,
        help_text=_("Owner can view, change or delete this location."),
    )
    location = models.ForeignKey(
        Location,
        verbose_name=_("Location"),
        related_name="object_location",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )

This is my current filters.py file which shows all the locations to the user.

# filters.py
from .models import Object
import django_filters

class ObjectFilter(django_filters.FilterSet):
    class Meta:
        model = Object
        fields = ["location", ]

This is the view which by default shows objects the user owns. It's possible to filter further by location. But the location drop-down shows too many entries.

# views.py
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Object
from .filters import ObjectFilter

class ObjectListView(LoginRequiredMixin, ListView):
    model = Object
    paginate_by = 10

    def get_queryset(self):
        queryset = Object.objects.filter(owner=self.request.user)
        filterset = ObjectFilter(self.request.GET, queryset=queryset)
        return filterset.qs

    def get_context_data(self, **kwargs):
        context = super(ObjectListView, self).get_context_data(**kwargs)
        filterset = ObjectFilter(self.request.GET, queryset=self.queryset)
        context["filter"] = filterset
        return context

My last attempt

I've tried to tweak the filters.py with adding a ModelChoiceFilter, but it ends up with an AttributeError: 'NoneType' object has no attribute 'request' .

# filters.py
from .models import Object
import django_filters

def get_location_queryset(self):
    queryset = Location.objects.filter(location__owner=self.request.user)
    return queryset

class ObjectFilter(django_filters.FilterSet):
    location = django_filters.filters.ModelChoiceFilter(queryset=get_location_queryset)

    class Meta:
        model = Object
        fields = ["location", ]

I believe a few different issues are at play here. First, as per the django-filter docs , when a callable is passed to ModelChoiceFilter , it will be invoked with Filterset.request as its only argument. So your filters.py would need to be rewritten like so:

# filters.py
from .models import Object
import django_filters

def get_location_queryset(request): # updated from `self` to `request`
    queryset = Location.objects.filter(location__owner=request.user)
    return queryset

class ObjectFilter(django_filters.FilterSet):
    location = django_filters.filters.ModelChoiceFilter(queryset=get_location_queryset)

    class Meta:
        model = Object
        fields = ["location", ]

This is half of the puzzle. I believe the other issue is in your view. django-filter has view classes that handle passing requests to filtersets, but this does not happen automatically using Django's generic ListView . Try updating your view code to something like this:

# views.py
from django_filters.views import FilterView

class ObjectListView(LoginRequiredMixin, FilterView): # FilterView instead of ListView
    model = Object
    filterset_class = ObjectFilter

This should take care of passing the request for you.

Also note that, as per the django-filter docs linked above, your queryset should handle the case when request is None . I've personally never seen that happen in my projects, but just FYI.

As an alternative, if you don't want to use FilterView , I believe the code in your example is almost there:

# views.py alternative
class ObjectListView(LoginRequiredMixin, ListView):
    model = Object
    paginate_by = 10

    def get_queryset(self):
        filterset = ObjectFilter(self.request)
        return filterset.qs

I think this would also work with the filters.py I specified above.

The problem with this code:

def get_location_queryset(self):
    queryset = Location.objects.filter(location__owner=self.request.user)
    return queryset

is that it is a function based view, you added self as an argument, and tried to access request which does not exist in context of self since self value is undefined to us

What i would do to filter out location based on user by creating a class based view for location filtering

class LocationView(ListView):
      def get_queryset(self):
          return Location.objects.filter(owner=self.request.user)

in filters.py:

class ObjectFilter(django_filters.FilterSet):
    location = django_filters.filters.ModelChoiceFilter(queryset=LocationView.as_view())

    class Meta:
        model = Object
        fields = ["location", ]

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM