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
, который я указал выше.