Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST Framework - POSTing foreign key field containing natural key?

I've recently started using the Django REST Framework (and Django, and Python - I'm an RTOS/embedded systems person!) in order to implement a RESTful Web API. Haven't had any problems yet which couldn't be resolved with Google, but this one has had me stumped for a few hours now.

I have an embedded system which listens for events which are associated with a range of devices - analogous to a Phone making Calls, which is what I'll discuss here for brevity. A Phone has a number and a whole lot of Calls (that it has made) associated with it. A Call has an associated Phone (the Phone which made the Call) and a time of creation. When a Call occurs, it should be POSTed to the API. I have an embedded system which listens for Calls and their originating phone number, and submits them to the API. Since the embedded system knows the phone number, I would like it to submit: {"srcPhone":12345678} rather than {"srcPhone":"http://host/phones/5"}. This avoids the need for my embedded system to know the primary key of every Phone (or to GET Phones by number every time it wants to submit a Call).

Google and the Django docs suggested I could achieve this with natural keys. My attempt follows:

models.py

from django.db import models
from datetime import datetime
from pytz import timezone
import pytz
from django.contrib.auth.models import User

# Create your models here.
def zuluTimeNow():
    return datetime.now(pytz.utc)


class PhoneManager(models.Manager):
    def get_by_natural_key(self, number):
        return self.get(number=number)


class Phone(models.Model):
   objects     = PhoneManager()
   number      = models.IntegerField(unique=True)

   #def natural_key(self):
   #    return self.number

   class Meta:
      ordering = ('number',)


class Call(models.Model):
    created    = models.DateTimeField(default=zuluTimeNow, blank=True)
    srcPhone   = models.ForeignKey('Phone', related_name='calls')

    class Meta:
        ordering = ('-created',)

views.py

# Create your views here.
from radioApiApp.models import Call, Phone
from radioApiApp.serializers import CallSerializer, PhoneSerializer
from rest_framework import generics, permissions, renderers
from rest_framework.reverse import reverse 
from rest_framework.response import Response
from rest_framework.decorators import api_view

@api_view(('GET',))
def api_root(request, format=None):
    return Response({
        'phones': reverse('phone-list', request=request, format=format),
        'calls': reverse('call-list', request=request, format=format),
    })


class CallList(generics.ListCreateAPIView):
    model = Call
    serializer_class = CallSerializer
    permission_classes = (permissions.AllowAny,)

class CallDetail(generics.RetrieveDestroyAPIView):
    model = Call
    serializer_class = CallSerializer
    permission_classes = (permissions.AllowAny,)

class PhoneList(generics.ListCreateAPIView):
   model = Phone
   serializer_class = PhoneSerializer
   permission_classes = (permissions.AllowAny,)

class PhoneDetail(generics.RetrieveDestroyAPIView):
   model = Phone
   serializer_class = PhoneSerializer
   permission_classes = (permissions.AllowAny,)

serializers.py

from django.forms import widgets
from rest_framework import serializers
from radioApiApp import models
from radioApiApp.models import Call, Phone

class CallSerializer(serializers.HyperlinkedModelSerializer):
   class Meta:
       model = Call
       fields = ('url', 'created', 'srcPhone')

class PhoneSerializer(serializers.HyperlinkedModelSerializer):
   calls = serializers.ManyHyperlinkedRelatedField(view_name='call-detail')
   class Meta:
      model = Phone
      fields = ('url', 'number', 'calls')

To test, I create a Phone with number 123456. Then I POST {"srcPhone":123456} to http://host/calls/ (which is configured in urls.py to run the CallList view). This gives an AttributeError at /calls/ - 'int' object has no attribute 'startswith'. The exception occurs in rest_framework/relations.py (line 355). Can post the entire trace if it'll be helpful. Upon reading relations.py, it looks like the REST Framework is not looking up Phones by number, but processing the srcPhone attribute as if it was a URL. This would normally be true, but I want it to look up Phones by natural key, rather than provide the URL. What have I missed here?

Thanks!

like image 289
EwanC Avatar asked Jan 16 '13 20:01

EwanC


2 Answers

What you're looking for is SlugRelatedField. See docs here.

but processing the srcPhone attribute as if it was a URL.

Exactly. You're using HyperlinkedModelSerializer, so the srcPhone key is using a hyperlink relation by default.

The 'int' object has no attribute 'startswith' exception you're seeing is because it's expecting a URL string, but receiving an integer. Really that ought to result in a descriptive validation error, so I've created a ticket for that.

If you instead use a serializer something like this:

class CallSerializer(serializers.HyperlinkedModelSerializer):
    srcPhone = serializers.SlugRelatedField(slug_field='number')

    class Meta:
        model = Call
        fields = ('url', 'created', 'srcPhone')

Then the 'srcPhone' key will instead represent the relationship using the 'number' field on the target of the relationship.

I'm planning on putting in some more work to the relationship documentation at some point soon, so hopefully this will be more obvious in the future.

like image 132
Tom Christie Avatar answered Oct 22 '22 13:10

Tom Christie


(Can't post this as a comment, too long)

Tom's answer above solved the problem.

However, I also want to have a hyperlinked field back to the Phone resource. The SlugRelatedField allows me to submit with an integer field belonging to the Phone, but when GETting the resulting Call resource, it also serialises as an integer. I'm sure this is intended functionality (doesn't seem very elegant to have it serialising from an integer, but to a hyperlink). The solution I found was to add another field to the CallSerializer: src = serializers.HyperlinkedRelatedField(view_name='phone-detail',source='srcPhone',blank=True,read_only=True) and add that field to the Meta class. Then I POST only srcPhone (an integer) and GET srcPhone plus src, which is a hyperlink to the Phone resource.

like image 33
EwanC Avatar answered Oct 22 '22 15:10

EwanC