Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django (Rest Framework): create (POST) a nested/sub-resource using hyperlinked relations

I'm having a hard time trying to understand how the hyperlinked serializers work. If I use normal model serializers it all works fine (returning id's etc). But I'd much rather return url's, which is a bit more RESTful imo.

The example I'm working with seems pretty simple and standard. I have an API which allows an 'administrator' to create a Customer (a company in this case) on the system. The Customer has attributes "name", "accountNumber" and "billingAddress". This is stored in the Customer table in the database. The 'administrator' is also able to create a Customer Contact (a person/point of contact for the Customer/company).

The API to create a Customer is /customer. When a POST is done against this and is successful, the new Customer resource is created under /customer/{cust_id}.

Subsequently, the API for creating a Customer Contact is /customer/{cust_id}/contact. When a POST Is done against this and is successful, the new Customer Contact resource is created under /customer/{cust_id}/contact/{contact_id}.

I think this is pretty straightforward and is a good example of a Resource Oriented Architecture.

Here are my models:

class Customer(models.Model):
    name = models.CharField(max_length=50)
    account_number = models.CharField(max_length=30, name="account_number")
    billing_address = models.CharField(max_length=100, name="billing_address")

class CustomerContact(models.Model):
    first_name = models.CharField(max_length=50, name="first_name")
    last_name = models.CharField(max_length=50, name="last_name")
    email = models.CharField(max_length=30)
    customer = models.ForeignKey(Customer, related_name="customer")

So there's a foreign key (many to one) relationship between the CustomerContact and Customer.

To create a Customer is pretty simple:

class CustomerViewSet(viewsets.ViewSet):
    # /customer POST
    def create(self, request):
        cust_serializer = CustomerSerializer(data=request.data, context={'request': request})
        if cust_serializer.is_valid():
            cust_serializer.save()
            headers = dict()
            headers['Location'] = cust_serializer.data['url']
            return Response(cust_serializer.data, headers=headers, status=HTTP_201_CREATED)
        return Response(cust_serializer.errors, status=HTTP_400_BAD_REQUEST)

Creating a CustomerContact is a bit trickier as I have to get the foreign key of the Customer, add it to the request data and pass that to the serializer (I'm not sure if this is the right/best way to do it).

class CustomerContactViewSet(viewsets.ViewSet):
    # /customer/{cust_id}/contact POST
    def create(self, request, cust_id=None):
        cust_contact_data = dict(request.data)
        cust_contact_data['customer'] = cust_id
        cust_contact_serializer = CustomerContactSerializer(data=cust_contact_data, context={'request': request})
        if cust_contact_serializer.is_valid():
            cust_contact_serializer.save()
            headers = dict()
            cust_contact_id = cust_contact_serializer.data['id']
            headers['Location'] = reverse("customer-resource:customercontact-detail", args=[cust_id, cust_contact_id], request=request)
            return Response(cust_contact_serializer.data, headers=headers, status=HTTP_201_CREATED)
        return Response(cust_contact_serializer.errors, status=HTTP_400_BAD_REQUEST)

The serializer for the Customer is

class CustomerSerializer(serializers.HyperlinkedModelSerializer):
     accountNumber = serializers.CharField(source='account_number', required=True)
    billingAddress = serializers.CharField(source='billing_address', required=True)
    customerContact = serializers.SerializerMethodField(method_name='get_contact_url')

    url = serializers.HyperlinkedIdentityField(view_name='customer-resource:customer-detail')

    class Meta:
        model = Customer
        fields = ('url', 'name', 'accountNumber', 'billingAddress', 'customerContact')

    def get_contact_url(self, obj):
        return reverse("customer-resource:customercontact-list", args=[obj.id], request=self.context.get('request'))

Note (and possibly ignore) the customerContact SerializerMethodField (I return the URL for CustomerContact in the representation of the Customer resource).

The serializer for the CustomerContact is:

class CustomerContactSerializer(serializers.HyperlinkedModelSerializer):
    firstName = serializers.CharField(source='first_name', required=True)
    lastName = serializers.CharField(source='last_name', required=True)

    url = serializers.HyperlinkedIdentityField(view_name='customer-resource:customercontact-detail')

    class Meta:
        model = CustomerContact
        fields = ('url', 'firstName', 'lastName', 'email', 'customer')

'customer' is the reference to the customer foreign key in the CustomerContact model/table. So when I do a POST like so:

POST http://localhost:8000/customer/5/contact
     body: {"firstName": "a", "lastName":"b", "email":"[email protected]"}

I get back:

{
    "customer": [
        "Invalid hyperlink - No URL match."
    ]
}

So it seems that foreign key relationships have to be expressed as URL's in HyperlinkedModelSerializer? The DRF tutorial (http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis/#hyperlinking-our-api) seems to say this too:

Relationships use HyperlinkedRelatedField, instead of PrimaryKeyRelatedField

I'm perhaps doing something wrong in my CustomerContactViewSet, is adding the customer_id to the request data before passing it to the serializer (cust_contact_data['customer'] = cust_id) incorrect? I tried passing it a URL instead - http://localhost:8000/customer/5 - from the POST example above, but I get a slightly different error:

{
    "customer": [
        "Invalid hyperlink - Incorrect URL match."
    ]
}

How do I use the HyperlinkedModelSerializer to create an entity which has a foreign key relationship with another model?

like image 725
Cliff Sun Avatar asked Jul 01 '15 10:07

Cliff Sun


1 Answers

Well, I dug a bit into the rest_framework and it seems that the mismatch is due to the URL pattern matching not resolving to your appropriate view namespace. Do some prints around here and you can see the expected_viewname not matching the self.view_name.

Check that your view namespacing is correct on your app (is seems these views are under namespace customer-resource), and if need be fix the view_name attribute on your relevant hyperlinked related fields via the extra_kwargs on the Serializer Meta:

class CustomerContactSerializer(serializers.HyperlinkedModelSerializer):
    firstName = serializers.CharField(source='first_name', required=True)
    lastName = serializers.CharField(source='last_name', required=True)

    url = serializers.HyperlinkedIdentityField()

    class Meta:
        model = CustomerContact
        fields = ('url', 'firstName', 'lastName', 'email', 'customer')
        extra_kwargs = {'view_name': 'customer-resource:customer-detail'}

Hope this works for you ;)

like image 160
joao figueiredo Avatar answered Sep 29 '22 09:09

joao figueiredo