The `.create()` method does not support writable n

2019-04-10 17:53发布

问题:

I have a big problem regarding the serialization of a Many to Many relationship with intermediate model in DRF: If the request method is get everything works perfectly. But as soon as i try to POST or PUT Data to the API I get the following Error:

Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/django/core/handlers/base.py", line 147, in get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/django/views/generic/base.py", line 68, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/views.py", line 477, in dispatch
    response = self.handle_exception(exc)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/views.py", line 437, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/views.py", line 474, in dispatch
    response = handler(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/generics.py", line 243, in post
    return self.create(request, *args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/mixins.py", line 21, in create
    self.perform_create(serializer)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/mixins.py", line 26, in perform_create
    serializer.save()
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/serializers.py", line 214, in save
    self.instance = self.create(validated_data)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/serializers.py", line 888, in create
    raise_errors_on_nested_writes('create', self, validated_data)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/djangorestframework-3.5.3-py2.7.egg/rest_framework/serializers.py", line 780, in raise_errors_on_nested_writes
    class_name=serializer.__class__.__name__
AssertionError: The `.create()` method does not support writable nested fields by default.
Write an explicit `.create()` method for serializer `manager.serializers.EquipmentSerializer`, or set `read_only=True` on nested serializer fields.

I am not really sure how to write proper create and update functions and i don´t really understand it, how it is explained in the documentation.

Code:

views.py:

from django.shortcuts import render 
from django.contrib.auth.models import User, Group
from manager.serializers import *
from rest_framework import generics 
from rest_framework import viewsets 
from rest_framework.decorators import api_view 
from rest_framework.response import Response 
from rest_framework.views import APIView  
from django.forms.models import model_to_dict



class OrderSetDetails(generics.RetrieveUpdateDestroyAPIView): 
    queryset = Order.objects.all() 
    serializer_class = OrderSerializer 

class OrderSetList(generics.ListCreateAPIView): 
    queryset = Order.objects.all() 
    serializer_class = OrderSerializer 

class EquipmentSetDetails(generics.RetrieveUpdateDestroyAPIView): 
    queryset = Equipment.objects.all() 
    serializer_class = EquipmentSerializer 

class EquipmentSetList(generics.ListCreateAPIView): 
    queryset = Equipment.objects.all() 
    serializer_class = EquipmentSerializer


class UserViewSet(viewsets.ModelViewSet):

    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer


class GroupViewSet(viewsets.ModelViewSet):

    queryset = Group.objects.all()
    serializer_class = GroupSerializer 

class ClientList(generics.ListCreateAPIView): 
    queryset = client.objects.all() 
    serializer_class = ClientSerializer  

serializers.py

from rest_framework import serializers  
from django.contrib.auth.models import User, Group
from storage.models import * 


class AssignmentSerializer(serializers.HyperlinkedModelSerializer): 
    id = serializers.ReadOnlyField(source = 'Order.id') 
    name = serializers.ReadOnlyField(source = 'Order.name') 

    class Meta:
        model = Assignment 
        fields = ('id', 'name', 'quantity') 


class EquipmentSerializer(serializers.ModelSerializer): 
    event = AssignmentSerializer(source= 'assignment_set', many = True)
    class Meta: 
        model = Equipment 
        fields = '__all__' 


class ClientSerializer(serializers.ModelSerializer): 

    class Meta: 
        model = client 
        fields = '__all__' 

class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ('url', 'username', 'email', 'groups')


class GroupSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Group
        fields = ('url', 'name') 

class OrderSerializer(serializers.ModelSerializer):
    class Meta: 
        model = Order 
        fields = '__all__' 

models.py:

from __future__ import unicode_literals

from django.db import models 
from storage.choices import *

# Create your models here.
class Equipment(models.Model): 
    name = models.CharField(max_length=30) 
    fabricator = models.CharField(max_length=30, default='-') 
    storeplace = models.IntegerField() 
    labor = models.CharField(max_length=1, choices=labor_choices) 
    event = models.ManyToManyField('Order', blank = True, through= 'Assignment', through_fields=('Equipment', 'Order')) 
    max_quantity = models.IntegerField(default=1, null = True) 
    status = models.CharField(max_length=8, choices = STATUS_CHOICES, default = 'im Lager') 





    def __str__(self): 
        return self.name 




class client(models.Model): 
    firstname = models.CharField(max_length=30) 
    secondname = models.CharField(max_length=30) 
    email = models.EmailField() 
    post_code = models.IntegerField()
    city = models.CharField(max_length=30) 
    street= models.CharField(max_length=30) 



    def __str__(self):              
        return "%s %s" % (self.firstname, self.secondname)



class Order(models.Model): 
    name = models.CharField(max_length=30) 
    Type = models.CharField(
        max_length=2,
        choices=TYPE_CHOICES,
        default='Rental', 
        )
    city = models.CharField(max_length=30) 
    street= models.CharField(max_length=30)
    date = models.DateField() 
    GuestNumber = models.IntegerField() 
    description = models.TextField() 
    client = models.ForeignKey("client", on_delete=models.CASCADE, blank = True, null = True) 
    status = models.CharField(max_length=30, choices=order_choices, default='glyphicon glyphicon-remove') 

    def __str__(self): 
        return self.name

class Assignment(models.Model): 
    Equipment = models.ForeignKey('Equipment',  on_delete=models.CASCADE) 
    Order = models.ForeignKey('Order',  on_delete=models.CASCADE) 
    quantity = models.PositiveIntegerField(default=1)

Thanks in Advance.

回答1:

DRF does not support create method for nested serializers. If you want to show related fields in a extended layout, and not only with pks then you could override the to_representation method instead of rewriting default mtm field. And you should override a create method, because of the third model in mtm link:

class EquipmentSerializer(serializers.ModelSerializer): 

    class Meta: 
        model = Equipment 
        fields = '__all__'

    def create(self, validated_data):
        order = Order.objects.get(pk=validated_data.pop('event'))
        instance = Equipment.objects.create(**validated_data)
        Assignment.objects.create(Order=order, Equipment=Equipment)
        return instance

    def to_representation(self, instance):
        representation = super(EquipmentSerializer, self).to_representation(instance)
        representation['assigment'] = AssignmentSerializer(instance.assigment_set.all(), many=True).data
        return representation 

Now it'll save mtm fields properly passing list of pks, like [1, 2, 3] and for representation of that mtm related model, EquipmentSerializer will use AssignmentSerializer.



回答2:

I think that the cause for error: JSON parse error - No JSON object could be decoded is because you forgot to put .data at the 2nd line from @Ivan Semochkin solution: representation['assigment'] = AssignmentSerializer(instance.assigment_set.all(), many=True).data.

Thus I find out that I will stumble upon Keyword Error: 'event' from line: representation = super(EquipmentSerializer, self).to_representation(instance) since the EquipmentSeralizer object contain the intermediary assignment_set instead of event.

Here are the end result adapting from @Ivan Semochkin solution I do. Correct me if I'm wrong/inappropriate in practice.

class EquipmentSerializer(serializers.ModelSerializer): 

    class Meta: 
        model = Equipment 
        fields = '__all__'

    def create(self, validated_data):
        order = Order.objects.get(pk=validated_data.pop('assignment_set').get('id'))
        instance = Equipment.objects.create(**validated_data)
        Assignment.objects.create(Order=order, Equipment=Equipment)
        return instance

    def to_representation(self, instance):
        representation = super(EquipmentSerializer, self).to_representation(instance)
        representation['assigment'] = AssignmentSerializer(instance.assigment_set.all(), many=True).data
        return representation 

Please correct me if I'm wrong. I'm new to Django.



回答3:

I think it is enough to add to your serializer which uses nested serializer attribute read_only=True like this:

class EquipmentSerializer(serializers.ModelSerializer): 
    event = AssignmentSerializer(
                source='assignment_set',
                many = True,
                read_only=True)          # read_only=True is added here
    class Meta: 
        model = Equipment 
            fields = '__all__'