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?
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.