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
Вернуться на верх