1

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?

AlejandroVK
  • 7,373
  • 13
  • 54
  • 77
  • Just `AccommodationSerializer(accommodation_data)`, no kwargs. How you put `product` in `accommodation_data` I'll leave to the answerer. – Brian Jul 08 '16 at 23:53
  • ProductAccommodationSerializer - Meta class... add `model = Product` – Jerzyk Jul 09 '16 at 01:52
  • I really do not like `class Meta(ProductSerializer.Meta)` – Jerzyk Jul 09 '16 at 01:53
  • Can you try `acc = AccommodationSerializer(data=product, context={'accommodation_data': accommodation_data})`, and share the result. – kapilsdv Jul 09 '16 at 07:41
  • @Brian that does not work I'm afraid. – AlejandroVK Jul 09 '16 at 07:56
  • @Jerzyk Does not work, and besides you need to pass the Meta of the parent Serializer to the children one, otherwise, this is in the DRF docs – AlejandroVK Jul 09 '16 at 07:57
  • @AlejandroVK what is not working, what error you are getting, in the second serializer remove specification of parent class for Meta - please read carefully – Jerzyk Jul 09 '16 at 08:03
  • @AlejandroVK where is your `ProductSerializer`? – Jerzyk Jul 09 '16 at 08:05
  • @Jerzyk Just added the ProductSerializer, I missed that one sorry – AlejandroVK Jul 09 '16 at 10:41
  • @KapilSachdev This does not work either, it does not fail (the error is gone) but the AccommodationSerializer is never called...try overriding the create method and was never executed, can you elaborate what your proposed solution does? – AlejandroVK Jul 09 '16 at 10:48
  • @Jerzyk I've removed the inheritance from ProductAccommodationSerializer in the Meta and added model = Product in that serializer but still getting the same error – AlejandroVK Jul 09 '16 at 10:57
  • Have you tried my solution without the context part ? If not, please try it once. _data_ is a valid keyword arg not product. – kapilsdv Jul 09 '16 at 13:14
  • @KapilSachdev tried and same results, the error goes away, but the AccommodationSerializer create method is never called... – AlejandroVK Jul 09 '16 at 16:25

1 Answers1

3

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.

Till Kolter
  • 602
  • 1
  • 9
  • 13
  • thanks for the detailed response, I've edited my post so please check it. I've tried adding the 'product' to the AccommodationSerializer but then I get this error. I've tried what you propose and it works, but I think I'm limiting myself a lot by instantiating the objects and not using the AccommodationSerializer. What happens if (as it is my case) I want to add more fields to that Serializer that are not even in the model (read-only) and return them? With your approach this would not be possible nope? – AlejandroVK Jul 09 '16 at 19:45
  • Will mark this as the correct answer, since this pointed me to the right direction, got it working now, thanks @Till – AlejandroVK Jul 09 '16 at 22:09
  • The thing is, that you don't need the `AccommodationSerializer` for creating the model. As far as I understood the DRF, serializers are not meant to be used inline to create distinct models inside creation methods of another model. Using custom and related object fields for read operations is alright, but if you do want to create/update more complex embedded structures, you will always have to implement you own `create` methods usually using model managers to create instances. – Till Kolter Jul 10 '16 at 23:31