Django-filter: ModelChoiceFilter с набором запросов на основе request.user

У меня есть основанный на классе Django ListView, в котором перечислены объекты. Эти объекты могут быть отфильтрованы на основе местоположений. Теперь я хочу, чтобы фильтр ModelChoiceFilter по местоположению перечислял только те местоположения, которые имеют отношение к текущему пользователю. Релевантные места - это места, которыми он владеет. Как я могу изменить набор запросов?

# 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,
    )

Вот мой текущий файл filters.py, который показывает все места пользователю.

# filters.py
from .models import Object
import django_filters

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

Это представление, которое по умолчанию показывает объекты, принадлежащие пользователю. Есть возможность дальнейшей фильтрации по местоположению. Но в выпадающем списке местоположения отображается слишком много записей.

# 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

Моя последняя попытка

Я пытался настроить filters.py, добавив ModelChoiceFilter, но это заканчивается 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", ]

Проблема с этим кодом:

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

заключается в том, что это представление на основе функции, вы добавили self в качестве аргумента, и попытались получить доступ к request, который не существует в контексте self, поскольку значение self для нас не определено

Что я должен сделать, чтобы отфильтровать местоположение на основе пользователя, создав представление на основе класса для фильтрации местоположения

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

в filters.py:

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

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

Я полагаю, что здесь имеет место несколько различных проблем. Во-первых, согласно документации django-filter docs, когда вызываемый фильтр передается в ModelChoiceFilter, он будет вызван с Filterset.request в качестве единственного аргумента. Поэтому ваш filters.py нужно будет переписать так:

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

Это половина головоломки. Я полагаю, что другая проблема кроется в вашем представлении. django-filter имеет классы представления, которые обрабатывают передачу запросов в наборы фильтров, но это не происходит автоматически при использовании общего ListView в Django. Попробуйте обновить код вашего представления на что-то вроде этого:

# views.py
from django_filters.views import FilterView

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

Это должно позаботиться о передаче запроса за вас.

Также обратите внимание, что согласно документации по django-filter, ссылка на которую приведена выше, ваш кверисет должен обрабатывать случай, когда request является None. Я лично никогда не сталкивался с этим в своих проектах, но просто к сведению.

В качестве альтернативы, если вы не хотите использовать FilterView, я считаю, что код в вашем примере почти готов:

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

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

Я думаю, что это также будет работать с filters.py, который я указал выше.

Вернуться на верх