1

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

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Hi @Monks, thanks a lot for your input! It started to work after I've switched from ListView to FilterView, only returned the queryset in def get_query_set(self) and removed the complete def get_context_data(self, **kwargs): method.
Ah, yeah, that makes sense. I forgot to consider how your existing get_queryset and get_context_data would interact with FilterView. Glad you got it working!
0

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

3 Comments

I tried to use my existing LocationListView(), but after applying it as you described I get an ImportError: cannot import name 'LocationListView' from partially initialized module 'inventory.views' (most likely due to a circular import) (/app/inventory/views.py).
You are importing file 1 to file 2, and file 2 to file 1, this is called "circular import", this is why you are getting the error.
Thanks @Ghazi, I was able to circle around the circular import issue. But now I get AttributeError at /object/: 'NoneType' object has no attribute 'user'. Do you have an other proposal on how to get the queryset? Full implementation is available in my GitHub repository. Last pushed version is without location = django_filters.filters.ModelChoiceFilter(queryset=LocationListView.as_view()) in filters.py

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.