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.action
s при посещении 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
)
- Ch. 2.1 (ListItem без
Книга 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):
Отключите браузерный API только для этого представления
Обновите 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