I am attempting to implement a nested resource where one of its fields depends on a value from its parent resource.
Suppose we are building a system for a company which provides information about its customers plus sales figures for the company's salespeople. So we have two models, Customer and Rep. A rep can sell to more than one customer.
URL that returns all customers: /api/1.0/customers/
URL for a specific customer: /api/1.0/customers/123/
URL for customer-specific information for a specific sales rep : /api/1.0/customers/123/rep/9/
Note the rep URL contains the customer ID as well as the rep ID.
I want the customer URL to return a nested resource containing summary information about the rep, plus a hyperlink to full customer-specific information for that rep. This is the output from the URL for all customers:
[
{
"id": 100,
"customer_name": "DolManSaxLil",
"rep": {
"id": 4,
"annual_sales": 1500.01,
"name": "Fred",
"url": "http://localhost:8000/api/1.0/customer/100/rep/4/"
}
},
{
"id": 200,
"customer_name": "Hotblack",
"rep": {
"id": 4,
"annual_sales": 10500.42,
"name": "Fred",
"url": "http://localhost:8000/api/1.0/customer/200/rep/4/"
}
}
]
To implement this we define two serializers:
class CustomerSummarySerializer(serializers.HyperlinkedModelSerializer):
id = ...
name = ...
rep = RepSummarySerializer(read_only=True)
class RepSummarySerializer(serializers.HyperlinkedModelSerializer):
id = ...
annual_sales = ...
name = ....
url = serializers.SerializerMethodField('get_rep_url')
The problem I am facing is that I cannot work out how to access the current customer.id from the function RepSummarySerializer.get_rep_url
. It's possible in a Detail view as the customer is held in self.parent.obj
:
def get_rep_url(self, obj):
customer_id = self.parent.obj.id
url = reverse('api_customer_rep',
kwargs={'customer_id': customer_id,
'rep_id': obj.id},
request=serializer.context.get('request'))
return url
However, in a list view, self.parent.obj
is a QuerySet of Customer objects and I can't see any way of identifying the current Customer. Is there any way of doing this? Have I missed something obvious?
Moment of clarity: the solution is to use a SerializerMethodField
to instantiate the RepSummarySerializer
and pass the customer_id
in the context:
class CustomerSummarySerializer(serializers.HyperlinkedModelSerializer):
id = ...
name = ...
rep = serializers.SerializerMethodField('get_rep')
def get_rep(self, obj):
rep = obj.rep
serializer_context = {'request': self.context.get('request'),
'customer_id': obj.id}
serializer = RepSummarySerializer(rep, context=serializer_context)
return serializer.data
The customer_id
can now be accessed in RepSummarySerializer.get_rep_url
like this:
def get_rep_url(self, obj):
customer_id = self.context.get('customer_id')
...
Don't know why I didn't think of this three hours ago.
In addition to the accepted answer, if you use viewsets and want your sub-resource to be a collection (filtered by the parent document) only, you can also use the @detail_route
decorator, as documented in the docs:
from rest_framework import viewsets
from rest_framework.decorators import detail_route
from rest_framework.response import Response
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSummarySerializer
...
@detail_route(methods=['get'])
def rep(self, request, pk=None):
customer = self.get_object()
queryset = customer.pk.all()
instances = self.filter_queryset(queryset)
serializer = RepSummarySerializer(instances,
context={'request': request}, many=True)
return Response(serializer.data)
Now you can query /customers/123/rep/
and you will get a list of all Rep
instances for the specified customer.
It probably won't fully solve what you need, but for many people that don't need full nested resources it's actually enough.
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