Django REST Frameworking: изменение кверисета PrimaryKeyRelatedField для GET-запросов
Возможно ли дать PrimaryKeyRelatedField
функцию get_queryset
, которая вызывается при выполнении GET запроса? В настоящее время она вызывается только при выполнении POST или PUT запроса, но я хочу иметь возможность ограничить, какие отношения будут показаны в GET запросе или в результате PUT или POST запроса.
Случай использования заключается в том, что у меня есть модель с отношениями "многие ко многим", к некоторым из которых пользователь имеет доступ, а к некоторым нет. Поэтому я хочу включить в результат только те, к которым пользователь имеет доступ.
В частности, возьмем такой пример:
class Quest(models.Model):
name = models.CharField(max_length=255)
is_completed = models.BooleanField(default=False)
characters = models.ManyToManyField(Character, blank=True)
campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE, editable=False)
class Character(models.Model):
name = models.CharField(max_length=255)
is_hidden = models.BooleanField(default=False)
campaign = models.ForeignKey(Campaign, on_delete=models.CASCADE, editable=False)
Итак, квест имеет массив символов, некоторые из которых могут быть помечены как скрытые.
Мой сериализатор выглядит следующим образом:
class QuestSerializer(DmNotesModelSerializer):
characters = CharacterPrimaryKeyRelatedField(many=True)
class Meta:
model = Quest
fields = "__all__"
class CharacterPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return Character.objects.filter(campaign_id=self.context.get("view").kwargs.get("campaign_id"))
Это делает массив персонажей доступным для записи, и я ограничиваю персонажей, которых вы можете добавить, теми, которые принадлежат к той же кампании, что и сам квест. Все работает отлично, вопросов нет.
Однако в настоящее время существует вероятность получения квеста, содержащего скрытые символы в массиве символов, что может вызвать проблемы в приложении (поиск такого символа приводит к 404). Поэтому я хочу ограничить набор квестов, чтобы удалить скрытые символы.
Я могу изменить свое представление, чтобы сделать фильтрацию следующим образом:
class QuestController(viewsets.ModelViewSet):
serializer_class = QuestSerializer
def get_queryset(self):
character_qs = Character.objects.all()
if not self.request.membership.is_dm:
character_qs = character_qs.filter(is_hidden=False)
return (
Quest.objects.filter(campaign_id=self.kwargs["campaign_id"])
.prefetch_related(Prefetch("character_set", queryset=character_qs))
)
def perform_create(self, serializer):
return serializer.save(campaign_id=self.kwargs["campaign_id"])
И это в основном работает: когда вы получаете квест, массив символов будет содержать только те символы, которые не скрыты. Но есть проблема: когда вы выполняете PUT-запрос для обновления квеста, ответ все равно будет содержать скрытые символы, поэтому мне придется внести еще больше изменений в представление, чтобы изменить сериализатор, который используется в ответе метода обновления.
И есть куча этих отношений "многие ко многим" в куче представлений, так что это быстро становится большим количеством добавляемого шаблона. Что возвращает меня к первоначальному вопросу: если бы можно было применять фильтры к набору запросов PrimaryKeyRelatedField
, используемому для возврата данных, все это было бы решено, насколько я могу судить. Итак, как я могу сделать нечто подобное?
Не уверен на 100%, что все понял, но я думаю, основываясь на QuestController.get_queryset
, что вам нужен доступ к request.membership.is_dm
в CharacterPrimaryKeyRelatedField.get_queryset
для того, чтобы заполнить is_hidden
.
Ну, вы уже используете self.context.get("view")
и таким же образом можете получить доступ к request
.
class CharacterPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
view = self.context.get("view")
request = self.context.get("request")
return Character.objects.filter(
campaign_id=view.kwargs.get("campaign_id"),
is_hidden=request.membership.is_dm,
)
А если request
отсутствует в контексте, его легко добавить, переопределив QuestController.get_serializer_context
; см. подробнее в Pass request context to serializer from Viewset in Django Rest Framework.
Вот что у меня получилось в итоге. Я знаю, что это очень халтурно и не рекомендуется, но это решает все мои проблемы без добавления большого количества шаблонов или кучи дополнительных запросов.
# managers.py
from django.db import models
is_dm = False
class HiddenObjectsManager(models.Manager):
def get_queryset(self):
queryset = super(HiddenObjectsManager, self).get_queryset()
if not is_dm:
queryset = queryset.filter(is_hidden=False)
return queryset
# models.py
class Character(models.Model):
objects = HiddenObjectsManager()
# ...
Затем в том же месте, где я сохраняю membership
на объект request
(в подклассе BasePermission
), я устанавливаю параметр is_dm
:
# permissions.py
from lib import managers
class CampaignMemberOrPublicReadOnlyPermission(BasePermission):
def has_permission(self, request, view, *args, **kwargs):
request.campaign = Campaign.objects.get(pk=view.kwargs.get("campaign_id"))
try:
request.membership = Membership.objects.get(user=request.user, campaign_id=view.kwargs.get("campaign_id"))
managers.is_dm = request.membership.is_dm
return True
except Membership.DoesNotExist:
request.membership = Membership()
managers.is_dm = False
# Not a member, then check if it's a public campaign, in which case we can do GET requests only
if not request.campaign.is_private:
return request.method in SAFE_METHODS
return False