Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST framework: Can a nested object access its parent object's details in a List View?

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?

like image 454
David Jones - iPushPull Avatar asked Aug 30 '13 18:08

David Jones - iPushPull


2 Answers

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.

like image 72
David Jones - iPushPull Avatar answered Oct 12 '22 23:10

David Jones - iPushPull


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.

like image 45
Danilo Bargen Avatar answered Oct 13 '22 00:10

Danilo Bargen