1

I have a problem while trying to create Nested object with a reverse relationship.

I'm trying to POST a Work but at the SAME time trying to POST a Price which is a Work child.

Here is my work_model:

from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
from PIL import Image

class Work(models.Model):
    user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
    name = models.CharField(max_length=200)
    length = models.IntegerField(null=True)
    width = models.IntegerField(null=True)

    def save(self, *args, **kwargs):
        super(Work, self).save(*args, **kwargs)

        img = Image.open(self.image.path)

        if img.height > 300 or img.width > 300:
            output_size = (300, 300)
            img.thumbnail(output_size)
            img.save(self.image.path)

    def __str__(self):
        return "{}".format(self.id)

My price_model:

from django.db import models
from .model_work import *
from .model_taxes import *
from djmoney.models.fields import MoneyField

class Price(models.Model):
    work = models.OneToOneField(Work, on_delete=models.CASCADE, related_name='price')
    price = MoneyField(max_digits=19, decimal_places=4, default_currency='USD', null=True)
    taxes = models.ForeignKey(Taxes, null=True, on_delete=models.SET_NULL)
    total = models.IntegerField(null=True)

    def __str__(self):
        return "{}".format(self.price)

And my taxes_model:

from django.db import models

class Taxes(models.Model):
    tax_percentage = models.IntegerField(null=True)
    tax_country = models.CharField(max_length=200, null=True)
    tax_region = models.CharField(max_length=200, null=True)

    def __str__(self):
        return "{}".format(self.tax_country)

Here is my work_serializer:

from rest_framework import serializers
from ..models.model_work import Work
from .serializers_user import *
from .serializers_price import *


class WorkIndexSerializer(serializers.ModelSerializer):
    """
    Serializer listing all Works models from DB
    """
    user = UserIndexSerializer()
    price = PriceDetailsSerializer(many=False)

    class Meta:
        model = Work
        fields = [
            'id',
            'user',
            'price',
            'name',
            'image',
            'length',
            'width'
        ]

class WorkCreateSerializer(serializers.ModelSerializer):
    """
    Serializer to create a new Work model in DB
    """

    price = PriceCreateSerializer(many=False)

    class Meta:
        model = Work
        fields = [
            'user',
            'price',
            'name',
            'length',
            'width'
        ]

    def create(self, validated_data):
        price = Price.objects.create(**validated_data)
        work = Work.objects.create(**validated_data)

        return work


class WorkDetailsSerializer(serializers.ModelSerializer):
    """
    Serializer showing details of an Works model from DB
    """
    user = UserIndexSerializer()

    class Meta:
        model = Work
        fields = fields = [
            'user',
            'name',
            'image',
            'length',
            'width'
        ]

My price_serializer:

from rest_framework import serializers
from ..models.model_price import Price
from .serializers_work import *

class PriceIndexSerializer(serializers.ModelSerializer):
    """
    Serializer showing Price information when called by Work GET serializers.
    Not showing 'work' field to avoid loop.
    """
    taxes = serializers.StringRelatedField(read_only=True)
    class Meta:
        model = Price
        fields = [
            'price',
            'price_currency',
            'taxes',
            'total'
        ]
        depth = 1

class PriceDetailsSerializer(serializers.ModelSerializer):
    """
    Serializer showing Price information when called by Work GET serializers.
    Not showing 'work' field to avoid loop.
    """
    taxes = serializers.StringRelatedField(read_only=True)
    class Meta:
        model = Price
        fields = [
            'price',
            'price_currency',
            'taxes',
            'total'
        ]
        depth = 1

class PriceCreateSerializer(serializers.ModelSerializer):
    """
    Serializer to create a new Price when new Work model is created in DB
    """
    work = serializers.StringRelatedField(read_only=True)
    taxes = serializers.StringRelatedField(read_only=True)
    class Meta:
        model = Price
        fields = [
            'work',
            'price',
            'price_currency',
            'taxes',
            'total'
        ]

    def create(self, validated_data):
        work = Work.objects.create(**validated_data)
        price = Price.objects.create(**validated_data)
        taxes = Taxes.objects.create(**validated_data)
        return price

And when I'm trying to POST a Work with Postman:

{
    "user":"2",
    "name":"work 20",
    "price": 
            {
                "price":20,
                "price_currency":"EUR",
                "taxes":1,
                "total":32
            },
    "length":"50",
    "width":"60"
}

I get the error:

TypeError at /works/
conversion from collections.OrderedDict to Decimal is not supported

And when I try to POST a Price by itself:

{
    "work":20,
    "price":20,
    "price_currency":"EUR",
    "taxes":1,
    "total":32
}

I get the error:

ValueError at /prices/
Cannot assign "<Money: 20.0000 EUR>": "Work.price" must be a "Price" instance.

I can't find any solution working with my case.

What am I doing worng or missing?

Thanks you for your responses!

MaxenceP
  • 291
  • 2
  • 12

1 Answers1

1
1. TypeError at /works/
conversion from collections.OrderedDict to Decimal is not supported

The solution is to pass the nested price dictionary to create Price model instead of the whole data:

class WorkCreateSerializer(serializers.ModelSerializer):

    price = PriceDetailsSerializer(many=False)

...

 def create(self, validated_data):
        price_data = validated_data.pop('price')
        work = Work.objects.create(**validated_data)
        price = Price.objects.create(**price_data)
        return price

2.

ValueError at /prices/
  Cannot assign "<Money: 20.0000 EUR>": "Work.price" must be a "Price" instance.

First, change work field on PriceCreateSerializer, so you can have work data in validated_data:

work = WorkDetailsSerializer(many=False)

then:

def create(self, validated_data):
        work_data = validated_data.pop('work')
        work = Work.objects.create(**work_data)
        price = Price.objects.create(**validated_data)
        return price
Igor Moraru
  • 7,089
  • 1
  • 12
  • 24
  • So basically the idea is than even though Price is Work's child, you need to create both instances at the same time and specify it to happen so with the "create" methods in their respective serializers? Is the following process correct: When trying to create a Work instance, it goes through the corresponding WorkCreateSerializer, in which it knows that the nested Price instance should be according to PriceCreateSerializer. Although in each you need to create instance of the other model and Django understands which is which? – Seb May 05 '20 at 02:52
  • 1
    Basically, yes. You are totally responsible for creating related model instances. Django serializers can't handle this automatically, but it can "understand" which object is which and validate it accordingly if you provide it the correct serializer. So, in parent serializer you have to call the corresponding serializer of the related instance to validate, and handle creation on your own. – Igor Moraru May 05 '20 at 07:22
  • @IgorMoraru I can't confirm that your answer works, because when I apply `work = WorkDetailsSerializer(many=False)` to `PriceCreateSerializer` it gives me the error `work = WorkDetailsSerializer(many=False) NameError: name 'WorkDetailsSerializer' is not defined` But I did import my `work_serializer` `from .serializers_work import *` – MaxenceP May 05 '20 at 09:08
  • The error is not related to the code, but maybe to the circular dependencies. You are importing price_serializers into work_serializer and work_serializers into price_serializer. – Igor Moraru May 05 '20 at 09:44
  • So does it mean that actually when I do "runserver" itt will initialise the different serializers according to their order in the _init_.py. But since I referenced the PriceCreateSerializer in the WorkCreateSerializer, but that the Work serializer is itself referenced in the Price serializer, it loops over serializers that at no point had time to be initialized? – MaxenceP May 05 '20 at 10:34