I'm trying to get rid of explicit binding between my models. Thus instead of using ForeignKey
, I'll use IntegerField
to just store the primary key of target model as a field. Thus I'll handle the relationship manually at code level. This is because, I'll have to move my some schemas to different database instances. So they can't have linkage.
Now, I'm facing issue with my nested serializer. I'm trying to create an instance of the below model:
17 class Customer(models.Model):
18 id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
19 phone_no = models.CharField(max_length=15, unique=True)
And this is my CustomerAddress
model, which references the Customer
model:
47 class CustomerAddress(models.Model):
48 # customer = models.ForeignKey(Customer, related_name='cust_addresses')
49 customer_id = models.UUIDField(default=uuid.uuid4)
50 address = models.CharField(max_length=1000)
Below is my serializers:
7 class CustomerAddressSerializer(serializers.ModelSerializer):
8
9 class Meta:
10 model = CustomerAddress
11 depth = 1
31 class CustomerSerializer(serializers.ModelSerializer):
32 cust_addresses = CustomerAddressSerializer(many=True)
33
34 class Meta:
35 model = Customer
36 depth = 1
37 fields = ('id', 'phone_no', 'cust_addresses',)
38
39 def create(self, validated_data):
40 cust = Customer.objects.create(id=uuid.uuid4())
41
42 for addr in validated_data['cust_addresses']:
43 address = addr['address']
44 cust_addr = CustomerAddress.objects.create(address=address, customer_id=cust.id)
45
46 return cust
My views:
12 class CustomerView(generics.RetrieveAPIView, generics.CreateAPIView):
13 serializer_class = CustomerSerializer
14
22 def get_object(self):
23 session = self.request.session
24 if session.has_key('uuid'):
25 id = session['uuid']
26 cust = Customer.objects.get(pk=uuid.UUID(id))
27 return cust
28 return None
When I try to fire the post request to above view from my test:
71 def test_create_customer_address(self):
72 cust_url = reverse('user_v1:customer')
73 # Now we create a customer, and use it's UID in the "customer" data of /cust-address/
74 cust_data = {"first_name": "Rohit", "last_name": "Jain", "phone_no": "xxxxxx", "email_id": "[email protected]", "cust_addresses": [{"city_id": 1, "address": "addr", "pin_code": "123124", "address_tag": "XYZ"}]}
75 cust_response = self.client.post(cust_url, cust_data, format='json')
76 print 'Post Customer'
77 print cust_response
78 self.assertEqual(cust_response.data['id'], str(cust_id))
My test is failing with the following error trace:
======================================================================
ERROR: test_create_customer_address (app.tests.CustomerViewTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/ubuntu/src/django-proj/app/tests.py", line 75, in test_create_customer_address
cust_response = self.client.post(cust_url, cust_data, format='json')
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 168, in post
path, data=data, format=format, content_type=content_type, **extra)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 90, in post
return self.generic('POST', path, data, content_type, **extra)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/compat.py", line 231, in generic
return self.request(**r)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 157, in request
return super(APIClient, self).request(**kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/test.py", line 109, in request
request = super(APIRequestFactory, self).request(**kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/test/client.py", line 466, in request
six.reraise(*exc_info)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 132, in get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
return view_func(*args, **kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/django/views/generic/base.py", line 71, in view
return self.dispatch(request, *args, **kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 456, in dispatch
response = self.handle_exception(exc)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 453, in dispatch
response = handler(request, *args, **kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/generics.py", line 190, in post
return self.create(request, *args, **kwargs)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/mixins.py", line 21, in create
headers = self.get_success_headers(serializer.data)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 470, in data
ret = super(Serializer, self).data
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 217, in data
self._data = self.to_representation(self.instance)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/serializers.py", line 430, in to_representation
attribute = field.get_attribute(instance)
File "/home/ubuntu/Envs/rj-venv/local/lib/python2.7/site-packages/rest_framework/fields.py", line 317, in get_attribute
raise type(exc)(msg)
AttributeError: Got AttributeError when attempting to get a value for field `cust_addresses` on serializer `CustomerSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Customer` instance.
Original exception text was: 'Customer' object has no attribute 'cust_addresses'.
----------------------------------------------------------------------
All this was working fine when I had my CustomerAddress
have ForeignKey
binding to Customer
. I'm not getting any clue on how to fix this thing. I tried to look through the source code to see whether some customization needs to be done. But I'm at loss. I feel like I've to somehow tweak with my serializer, by may be overriding to_representation
method, but I'm not sure.
BTW, error only comes while creating the model instance. For GET
request, I get the proper json, with nested serializer.
Have anyone else tried to do something like this and succeeded? What should be done to make this work? And yes, I've to remove the explicit foreign key binding.
I would take REST framework out of the equation, and initially just look at how you want to manage this relationship at a model and manager/queryset level.
I'd start with what you have in your own answer, but perhaps make cust_addresses
be a cached property, so you can prevent multiple lookups.
class Customer(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
phone_no = models.CharField(max_length=15, unique=True)
# Only make the relationship query once.
@cached_property
def cust_addresses(self):
return CustomerAddress.objects.filter(customer_id=self.id)
class CustomerAddress(models.Model):
customer_id = models.UUIDField(default=uuid.uuid4)
address = models.CharField(max_length=1000)
You can then consider adding pre-caching behavior in a customer manager/queryset class. For example, you could pre-cache the relationship when creating both a customer and their set of addresses:
class CustomerManager(models.Manager):
def create(phone_no, cust_addresses):
"""
Customers and addresses are always created together.
Usage:
CustomerManager.objects.create(
phone_no='123',
cust_addresses=[{'address': 'abc'}, {'address': 'def'}]
)
"""
customer = super(CustomerManager, self).create(phone_no=phone_no)
addresses = [
CustomerAddress.objects.create(
customer_id=instance.id
address=item['address']
)
for item in cust_addresses
]
# When creating both Customer and Address instances,
# we can pre-cache the relationship.
customer.__dict__['cust_addresses'] = addresses
return customer
Make sure to add objects = CustomerManager()
to the Customer
model class.
Then you just have the integration with REST framework serializers to deal with.
Note that I've dropped using ModelSerializer
in favor of a plain Serializer
class. For everything other than quick prototyping I'd tend to prefer this - what you lose in duplication you gain in simplicity and clarity.
class CustomerAddressSerializer(serializers.Serializer):
address = serializers.CharField(max_length=1000)
class CustomerSerializer(serializers.Serializer):
id = serializers.UUIDField(read_only=True)
phone_no = serializers.CharField(max_length=15, validators=[UniqueValidator(queryset=Customer.objects.all())])
cust_addresses = CustomerAddressSerializer(many=True)
def create(self, validated_data):
return Customer.objects.create(**validated_data)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With