While trying to post mptt model via django rest framework getting ValueError: Cannot use None as a query value

While trying to post mptt model via django rest framework getting ValueError: Cannot use None as a query value

这是我尝试为其编写测试的视图:

class RestaurantsTreeView(generics.ListCreateAPIView):
    serializer_class = RestarauntsTreeSerializer

    def get_serializer_class(self):
        from rest_framework import serializers
        if self.request.method == 'GET':
            return RestarauntsTreeSerializer
        parent_choices = self.request.user.restaurants_set.filter(status=Restaurants.VISIBLE)

        class NestedRestaurantDetailSerializer(serializers.ModelSerializer):
            parent_id = serializers.RelatedField(queryset=parent_choices, allow_null=True, required=False)

            class Meta:
                model = Restaurants
                fields = ("id", "name", "parent_id")

        return NestedRestaurantDetailSerializer

这是我试图通过 POST 请求创建的模型:

from mptt.models import MPTTModel, TreeForeignKey


class Restaurants(MPTTModel):
    name = models.CharField(max_length=255, blank=True, null=True)
    parent = TreeForeignKey('self', null=True, blank=True, related_name='children', db_index=True)

    class MPTTMeta:
        order_insertion_by = ['name']

    def __str__(self):
        return self.name

最后是我的测试:

class CreateRestaurantTestCase(TestCase):
    def setUp(self):
        self.user = UserFactory.create()
        self.user.user_permissions.add(Permission.objects.get(codename='add_restaurants'))
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_authorization_required(self):
        response = self.client.post(reverse('api_v1:restaurants_list'), data={
            "parent_id": None,
            "name": "fake restaurant",
        })
        self.assertEqual(response.status_code, 401)

那个returns那个错误:

Error
Traceback (most recent call last):
  File "project/cafe/tests/api/test_restaurants.py", line 26, in test_required_fields
    response = self.client.post(reverse('api_v1:restaurants_list'), data={})
  File "env/lib/python3.5/site-packages/rest_framework/test.py", line 299, in post
    path, data=data, format=format, content_type=content_type, **extra)
  File "env/lib/python3.5/site-packages/rest_framework/test.py", line 212, in post
    return self.generic('POST', path, data, content_type, **extra)
  File "env/lib/python3.5/site-packages/rest_framework/test.py", line 237, in generic
    method, path, data, content_type, secure, **extra)
  File "env/lib/python3.5/site-packages/django/test/client.py", line 416, in generic
    return self.request(**r)
  File "env/lib/python3.5/site-packages/rest_framework/test.py", line 288, in request
    return super(APIClient, self).request(**kwargs)
  File "env/lib/python3.5/site-packages/rest_framework/test.py", line 240, in request
    request = super(APIRequestFactory, self).request(**kwargs)
  File "env/lib/python3.5/site-packages/django/test/client.py", line 501, in request
    six.reraise(*exc_info)
  File "env/lib/python3.5/site-packages/django/utils/six.py", line 686, in reraise
    raise value
  File "env/lib/python3.5/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "env/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "env/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "env/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "env/lib/python3.5/site-packages/django/views/generic/base.py", line 68, in view
    return self.dispatch(request, *args, **kwargs)
  File "env/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "env/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "env/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "env/lib/python3.5/site-packages/rest_framework/generics.py", line 244, in post
    return self.create(request, *args, **kwargs)
  File "env/lib/python3.5/site-packages/rest_framework/mixins.py", line 21, in create
    self.perform_create(serializer)
  File "env/lib/python3.5/site-packages/rest_framework/mixins.py", line 26, in perform_create
    serializer.save()
  File "env/lib/python3.5/site-packages/rest_framework/serializers.py", line 214, in save
    self.instance = self.create(validated_data)
  File "env/lib/python3.5/site-packages/rest_framework/serializers.py", line 913, in create
    instance = ModelClass.objects.create(**validated_data)
  File "env/lib/python3.5/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "env/lib/python3.5/site-packages/django/db/models/query.py", line 394, in create
    obj.save(force_insert=True, using=self.db)
  File "env/lib/python3.5/site-packages/mptt/models.py", line 977, in save
    right_sibling = opts.get_ordered_insertion_target(self, parent)
  File "env/lib/python3.5/site-packages/mptt/models.py", line 216, in get_ordered_insertion_target
    queryset = node.__class__._tree_manager.db_manager(node._state.db).filter(filters).order_by(*order_by)
  File "env/lib/python3.5/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "env/lib/python3.5/site-packages/django/db/models/query.py", line 784, in filter
    return self._filter_or_exclude(False, *args, **kwargs)
  File "env/lib/python3.5/site-packages/django/db/models/query.py", line 802, in _filter_or_exclude
    clone.query.add_q(Q(*args, **kwargs))
  File "env/lib/python3.5/site-packages/django/db/models/sql/query.py", line 1250, in add_q
    clause, _ = self._add_q(q_object, self.used_aliases)
  File "env/lib/python3.5/site-packages/django/db/models/sql/query.py", line 1270, in _add_q
    current_negated, allow_joins, split_subq)
  File "env/lib/python3.5/site-packages/django/db/models/sql/query.py", line 1276, in _add_q
    allow_joins=allow_joins, split_subq=split_subq,
  File "env/lib/python3.5/site-packages/django/db/models/sql/query.py", line 1160, in build_filter
    value, lookups, used_joins = self.prepare_lookup_value(value, lookups, can_reuse, allow_joins)
  File "env/lib/python3.5/site-packages/django/db/models/sql/query.py", line 989, in prepare_lookup_value
    raise ValueError("Cannot use None as a query value")
ValueError: Cannot use None as a query value

Destroying test database for alias 'default'...

Process finished with exit code 1

我希望能够创建父字段为空的餐厅。它可以是 post 没有父字段或有空的父字段。谢谢 django 1.11、drf 3.7、mptt 0.8.7

首先,您将序列化器定义放在视图中这一事实使您的代码不太可读...您可以修改 parent_id 字段的查询集,同时覆盖 get_serializer 方法你的看法。

# serializers.py
from rest_framework import serializers

class NestedRestaurantDetailSerializer(serializers.ModelSerializer):
    # We don't care about filtering the queryset here, we override it in the view
    parent_id = serializers.RelatedField(queryset=Restaurant.objects.all(), allow_null=True, required=False)

    class Meta:
        model = Restaurants
        fields = ("id", "name", "parent_id")

# views.py
from .serializers import (
    RestaurantsTreeSerializer,
    NestedRestaurantDetailSerializer
)

class RestaurantsTreeView(generics.ListCreateAPIView):
    def get_serializer(self, *args, **kwargs):
        if self.request.method == 'GET':
            kwargs['context'] = self.get_serializer_context()
            return RestaurantsTreeSerializer(*args, **kwargs)
        else:
            kwargs['context'] = self.get_serializer_context()
            nested_serializer = NestedRestaurantDetailSerializer(*args, **kwargs)

            # Here we modify the queryset of the `parent_id` field
            nested_serializer.fields['parent_id'].queryset = self.request.user.restaurants_set.filter(status=Restaurants.VISIBLE)
        
            return nested_serializer

其次,关于您的异常,在documentation of django-mptt中,您可以阅读以下内容:

order_insertion_by

A list of field names which should define ordering when new tree nodes are being inserted or existing nodes are being reparented, with the most significant ordering field name first. Defaults to [].

It is assumed that any field identified as defining ordering will never be NULL in the database.

上面是重要的一行。在您的模型定义中,您有:

name = models.CharField(max_length=255, blank=True, null=True)

您将可为空的字段定义为 order_insertion_by 属性的参数。你最后的例外是 Cannot use None as a query value 的事实让我认为它是有联系的...

未显示异常起源处的测试方法代码,但我假设您使用 test_required_fields 之类的名称尝试向您的 enpoint 发送一个空的 name 参数。因此 Cannot use None as a query value。由于您在模型中将 name 字段定义为 nullable,因此您的序列化程序也将其标记为 nullable