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!
What you’re looking for is
SlugRelatedField. See docs here.Exactly. You’re using
HyperlinkedModelSerializer, so thesrcPhonekey 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:
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.