Search code examples
pythondjangodjango-rest-frameworkdrf-nested-routers

Django REST framework serializer error when instantiating


I'm having an strange issue with DRF ans some serializers.

Here is my model:

class Accommodation(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    product = models.OneToOneField(
        Product,
        on_delete=models.CASCADE,
        primary_key=True,
    )
    description = models.TextField(null=True, blank=True, verbose_name=_(u'Description'))
    shared_accommodation = models.BooleanField(default=False)
    accommodation_unit_quantity = models.PositiveSmallIntegerField(default=1,
                                                                verbose_name=_(u'Number of acommodation units '
                                                                              u'for this acommodation'))
    accommodation_unit_name = models.TextField(null=False, blank=False, verbose_name=_(u'Name for accommodation units '
                                                                                   u'for this accommodation'))

    class Meta:
        verbose_name_plural = _(u'Accommodations')

    def __unicode__(self):
        return u'{0} <{1}>'.format(self.product.name, self.product.school)

class Product(AbstractProduct):

    name = models.CharField(max_length=50, verbose_name=_(u'Name'))
    school = models.ForeignKey('school.School')
    levels = models.ManyToManyField('school.Level',verbose_name=_(u'Level'))
    age = IntegerRangeField(null=True)
    gender = models.CharField(choices=GENDER_CHOICES, max_length=1, null=True, blank=True, verbose_name=_(u'Gender'))
    num_sessions = models.PositiveSmallIntegerField(
        verbose_name=_(u'Number of sessions'),
        default=1,
        help_text=_(u"Number of sessions that the product has."),
    )
    school_category = models.ForeignKey(
        'school.Category',
        blank=True, null=True,
        verbose_name=_(u'Category')
    )
    addons = models.ManyToManyField('self',
        verbose_name=_(u'Administrators'),
        through='AddonInService',
        symmetrical=False,
        related_name='addon_can_be_used_in'
    )

    pay_option = models.CharField(choices=PAYMENT_OPTIONS, max_length=1, null=True, blank=True, verbose_name=_(u'Pay_option'), default='U')
    payment_type = models.CharField(choices=PAYMENT_TYPE, max_length=1, null=True, blank=True, verbose_name=_(u'pay_type'))
    payment_amount = models.FloatField(verbose_name=_(u'Amount'), default=0.0)

    objects = ProductManager()

    class Meta(AbstractProduct.Meta):
        verbose_name_plural = _(u'Products')

    def __unicode__(self):
        return self.name

As you can see, basically a Product can be an Accommodation. Here are the Serializers

class AccommodationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Accommodation
        fields = [
            'description',
            'shared_accommodation',
            'accommodation_unit_quantity',
            'accommodation_unit_name',
        ]


class ProductAccommodationSerializer(ProductSerializer):

    accommodation = AccommodationSerializer()

    class Meta(ProductSerializer.Meta):
        fields = [
            'id',
            'structure',
            'upc',
            'title',
            'slug',
            'description',
            'rating',
            'date_created',
            'date_updated',
            'is_discountable',
            'name',
            'age',
            'gender',
            'num_sessions',
            'parent',
            'product_class',
            'school',
            'levels',
            'school_category',
            'addons',
            'color',
            'price',
            'all_prices',
            'variants',
            'pay_option',
            'payment_type',
            'payment_amount',
            'accommodation',
        ]

    def create(self, validated_data):
        accommodation_data = validated_data.pop('accommodation')

        levels = []
        if 'levels' in validated_data:
            levels = validated_data.pop('levels')

        product = Product.objects.create(**validated_data)
        school_accommodation, created = ProductClass.objects.get_or_create(name='School Accommodation')
        if created:
            product.product_class = school_accommodation
        for lev in levels:
            product.levels.add(lev)
        product.save()

        acc = AccommodationSerializer(product=product, **accommodation_data)
        acc.save()
        return product

class ProductSerializer(serializers.ModelSerializer):
    age = IntegerRangeField()
    addons = AddonSerializer(many=True, read_only=True)
    # Get the price for the Product, using the property in the Model
    price = serializers.DecimalField(required=False, max_digits=7,
                                 decimal_places=2, source='get_price',
                                 read_only=True)
    color = serializers.SerializerMethodField()

    all_prices = PriceSerializer(source='stockrecords', many=True,
                                   required=False)

    variants = VariantSerializer(many=True, source='children', required=False)

    class Meta:
        model = Product
        fields = [
            'id',
            'structure',
            'upc',
            'title',
            'slug',
            'description',
            'rating',
            'date_created',
            'date_updated',
            'is_discountable',
            'name',
            'age',
            'gender',
            'num_sessions',
            'parent',
            'product_class',
            'school',
            'levels',
            'school_category',
            'addons',
            'color',
            'price',
            'all_prices',
            'variants',
            'pay_option',
            'payment_type',
            'payment_amount'
        ]

Performing a simple test where I try to create an Accommodation, I get the following error:

Traceback (most recent call last):
File "/home/internetmosquito/git/mvp_opencoast/opencoast_django/opencoast/applications/accommodation/tests/test_accommodations.py", line 165, in test_create_accommodation
response = self.client.post(url, data, format='json')
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/test.py", line 170, in post
path, data=data, format=format, content_type=content_type, **extra)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/test.py", line 92, in post
return self.generic('POST', path, data, content_type, **extra)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/django/test/client.py", line 380, in generic
return self.request(**r)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/test.py", line 159, in request
return super(APIClient, self).request(**kwargs)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/test.py", line 111, in request
request = super(APIRequestFactory, self).request(**kwargs)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/django/test/client.py", line 467, in request
six.reraise(*exc_info)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 149, in get_response
response = self.process_exception_by_middleware(e, request)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 147, in get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
return view_func(*args, **kwargs)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/viewsets.py", line 87, in view
return self.dispatch(request, *args, **kwargs)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/views.py", line 466, in dispatch
response = self.handle_exception(exc)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/views.py", line 463, in dispatch
response = handler(request, *args, **kwargs)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/mixins.py", line 21, in create
self.perform_create(serializer)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/mixins.py", line 26, in perform_create
serializer.save()
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 191, in save
self.instance = self.create(validated_data)
File "/home/internetmosquito/git/mvp_opencoast/opencoast_django/opencoast/applications/accommodation/serializers.py", line 77, in create
acc = AccommodationSerializer(product=product, **accommodation_data)
File "/home/internetmosquito/python_envs/opencoast_django/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 95, in __init__
super(BaseSerializer, self).__init__(**kwargs)
TypeError: __init__() got an unexpected keyword argument 'product'

Tried to remove the

product=product

From

acc = AccommodationSerializer(product=product, **accommodation_data)

But then I get the same error but with 'shared_accommodation' field instead of product

WHat I'm doing wrong here? Any ideas?

EDIT: Added ProductSerializer, I missed that one sorry

SECOND EDIT: As suggested by some, I've added the product field to the AccommodationSerializer:

class AccommodationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Accommodation
        fields = [
            'product',
            'description',
            'shared_accommodation',
            'accommodation_unit_quantity',
            'accommodation_unit_name',
        ]

But then when trying to create an instance, I get the following error:

{'accommodation': OrderedDict([('product', [u'This field is required.'])])}

Funny enough, if I add the product to the test data payload (even though I haven't created the product at the time I call the endpoint to make an Accommodation, the error above dissappears):

data = {
        "name": "Hotel Hudson",
        "slug": "hotel-hudson",
        "age": {'upper': 99, 'lower': 18},
        "school": school1.id,
        "levels": [school1.level_set.all()[0].id],
        "accommodation": {
            "product": 1,
            "description": "A very nice hotel",
            "shared_accommodation": False,
            "accommodation_unit_quantity": 1,
            "accommodation_unit_name": "Room",
            "accommodation_units": [
                {
                    'name': "Room-1",
                    'max_pax': 1,
                },
                {
                    'name': "Room-2",
                    'max_pax': 3,
                },
            ]
        },
    }

While this is interesting, this is obviously not what I want...I don't want having to pass a fake product ID when calling the endpoint to create an Accommodation...any pointers?


Solution

  • Using the data field would be the right way since the keywords in the DRF Serializer hierarchy are not generic. If the dictionary you specifiy for data is valid you could create a model instance with .save() (after calling .is_valid()). The dictionary could of course be augmented with further attributes before creating the model. But beware, that the serializer only uses the attributes, which are specied in the Meta.fields field of the serializer.

    And here is the critical point, why your approach would not work after all: the AccomodationSerializer.Meta.fields does not include the product field, which is mandatory if you want to create a model.

    It is fine to use AccommodationSerializer to read from the Accommodation model or if you want to post a partial structure of the model for some reason. But if you want to use it to create a model instance, you have to specify all fields which are not nullable or have a default value.

    Instead of using the AccommodationSerializer here, you could just call:

        Accommodation.objects.create(product=product, **accommodation_data)
    

    I tried to set up a minimal example. Hope this helps.

    models.py:

    class Owner(models.Model):
    
        owner_name = models.CharField(max_length=255)
    
    
    class Product(models.Model):
    
        name = models.CharField(max_length=255)
        owner = models.OneToOneField(Owner)
    

    serializer.py

    class OwnerSerializer(serializers.ModelSerializer):
    
        class Meta:
            model = Owner
            fields = [
                'owner_name',
            ]
    
    class ProductSerializer(serializers.ModelSerializer):
    
        owner = OwnerSerializer(read_only=True)
    
        class Meta:
            model = Product
            fields = [
                'owner',
                'name',
            ]
    
    
    class ProductOwnerSerializer(serializers.ModelSerializer):
    
        product = ProductSerializer()
    
        class Meta:
            model = Owner
            fields = [
                'product',
                'owner_name',
            ]
    
        def create(self, validated_data):
            product_data = validated_data.pop('product')
            owner = Owner.objects.create(**validated_data)
            Product.objects.create(owner=owner, **product_data)
            return owner
    

    I also agree with Jerzyk's comment, that I really don't like the Meta(Superclass), seems like an anti-pattern to me.