gldecurtins
gldecurtins

Reputation: 47

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", ]

Upvotes: 1

Views: 1585

Answers (2)

Monks
Monks

Reputation: 56

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.

Upvotes: 2

Ghazi
Ghazi

Reputation: 804

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", ]

Upvotes: 0

Related Questions