DRF Browserable API запускает другие self.actions - которые в свою очередь запускают нерелевантные разрешения, что приводит к KeyError

У меня есть DRF ViewSet, который, похоже, вызывает класс разрешения "create" (IsListOwner ниже), когда я просто хочу получить представление списка.

urls.py

router = DefaultRouter()
router.register(r"list-items", ListItemViewSet, basename="list_item")

views.py

class ListItemViewSet(viewsets.ModelViewSet):
    queryset = ListItem.objects.all()

    def get_serializer_class(self):
        if self.action == "retrieve":
            return ListItemDetailSerializer
        if self.action == "create":
            return ListItemCreateSerializer
        return ListItemListSerializer

    def get_permissions(self):
        print("self.action", self.action)
        permission_classes = [AllowAny]
        if self.action == "list" or self.action == "retrieve":
            permission_classes = [AllowAny]
        elif (
            self.action == "create"
            or self.action == "update"
            or self.action == "partial_update"
            or self.action == "destroy"
        ):
            permission_classes = [IsListItemOwner]
        return [permission() for permission in permission_classes]

Когда я перехожу на сайт http://localhost:8000/api/v1/list-items/, я получаю следующую ошибку:

web_1  | Internal Server Error: /api/v1/list-items/
web_1  |     view.check_permissions(request)
web_1  |   File "/usr/local/lib/python3.9/site-packages/rest_framework/views.py", line 332, in check_permissions
web_1  |     if not permission.has_permission(request, self):
web_1  |   File "/project/app/permissions.py", line 60, in has_permission
web_1  |     return List.objects.get(pk=request.data["list_id"]).user == request.user
web_1  | KeyError: 'list_id'
web_1  | [27/Aug/2021 01:22:14] "GET /api/v1/list-items/ HTTP/1.1" 500 126003

Что указывает на разрешение "IsListOwner" в моем файле permissions.py. (Несмотря на то, что единственным явным self.action является "list")

Я понимаю, что это происходит потому, что браузерный API DRF инициализирует сериализаторы для HTML-форм. Если я зарегистрирую self.actions при посещении http://localhost:8000/api/v1/list-items/, я могу подтвердить, что и "list" и "create" срабатывают.

Таким образом, эта KeyError будет обойдена, если я использую http://localhost:8000/api/v1/list-items/?format=json (и будет срабатывать только "список")

Но как я могу продолжать использовать браузерный API без получения этой KeyError?

Ниже приведены мои конкретные models.py, serializers.py и permissions.py:

У меня есть модель List и модель ListItem.

ListItems либо принадлежат Списку, либо другому ListItem.

e.g.

  • Мой список (List)

    • Книга 1 (ListItem с "My List" в качестве list)

      • Глава 1 (ListItem без list и "Книга 1" в качестве parent)

      • Глава 2 (ListItem без list и "Книга 1" в качестве parent)

        • Ch. 2.1 (ListItem без list и "Ch. 2" в качестве parent)
    • Книга 2 (ListItem с "My List" в качестве list)

    • Книга 3 (ListItem с "My List" в качестве list)

Все ListItems в конечном итоге приведут к List, если вы перейдете к корневому родителю. Это можно сделать с помощью метода MPTT get_root(), т.е. .get_root().list

models.py

class List(models.Model):
    id = models.UUIDField(
        default=generate_ulid_as_uuid, primary_key=True, editable=False
    )
    title = models.CharField(max_length=255)
    user = models.ForeignKey(User, null=True, blank=True, on_delete=models.CASCADE)
    slug = models.JSONField(unique=True)


class ListItem(LifecycleModelMixin, MPTTModel):
    id = models.UUIDField(
        default=generate_ulid_as_uuid, primary_key=True, editable=False
    )
    list = models.ForeignKey(
        List, related_name="list_items", null=True, blank=True, on_delete=models.CASCADE
    )
    order = models.PositiveSmallIntegerField(null=True, blank=True)

    slug = models.JSONField(unique=True)

    parent = TreeForeignKey(
        "self", related_name="children", null=True, blank=True, on_delete=models.CASCADE
    )

    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.UUIDField()
    content_object = GenericForeignKey(
        "content_type", "object_id", for_concrete_model=False
    )

    @hook(AFTER_CREATE)
    def create_list_item_children(self):
        item = self.content_object
        # if item has children
        if not item.is_leaf_node():
            immediate_children = item.get_children()
            for child in immediate_children:
                ListItem.objects.create(parent=self, content_object=child)

serializers.py

class ListItemDetailSerializer(serializers.ModelSerializer):
    // various fields and SerializerMethodFields for associated Lists and other models
    ...


class ListItemCreateSerializer(serializers.ModelSerializer):
    list_id = serializers.UUIDField(required=True)
    object_id = serializers.UUIDField(required=True)

    class Meta:
        model = ListItem
        fields = ["order", "list_id", "object_id"]

    def create(self, validated_data):
        object_id = validated_data.get("object_id")
        content_type = ContentType.objects.get_for_model(
            Text.objects.get(pk=object_id), for_concrete_model=False
        )
        validated_data["content_type"] = content_type
        return super(ListItemCreateSerializer, self).create(validated_data)


class ListItemListSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = ListItem
        fields = ["pk", "slug"]

permissions.py

class IsListItemOwner(permissions.BasePermission):
    def has_permission(self, request, view):
        if view.action == "create":
            return List.objects.get(pk=request.data["list_id"]).user == request.user
        else:
            return True

    def has_object_permission(self, request, view, obj):
        return obj.get_root().list.user == request.user

Несколько обходных решений (помимо полного отключения браузерного API):

  1. Отключите браузерный API только для этого представления

  2. Обновите permissions.py с определенным условием для "list_id":

class IsListItemOwner(permissions.BasePermission):
    def has_permission(self, request, view):
        if view.action == "create":
            if not "list_id" in request.data:
                # prevent "TypeError: list_id" for DRF browserable API
                return False
            return List.objects.get(pk=request.data["list_id"]).user == request.user
        else:
            return True

    def has_object_permission(self, request, view, obj):
        return obj.get_root().list.user == request.user
Вернуться на верх