2013-01-16 18 views
12

Ho recentemente iniziato a utilizzare il Django REST Framework (e Django e Python - Sono un utente di sistemi RTOS/embedded!) Per implementare un'API Web RESTful. Non ho ancora avuto problemi che non possano essere risolti con Google, ma questo mi ha costretto per un paio di ore.Django REST Framework - POSTing chiave esterna contenente campo chiave naturale?

Ho un sistema incorporato che ascolta gli eventi associati a una gamma di dispositivi, analogamente a un telefono che effettua chiamate, che è quello che discuterò qui per brevità. Un telefono ha un numero e un sacco di chiamate (che ha fatto) associate ad esso. Una chiamata ha un telefono associato (il telefono che ha effettuato la chiamata) e un orario di creazione. Quando si verifica una chiamata, è necessario eseguire il POST sull'API. Ho un sistema incorporato che ascolta le chiamate e il loro numero di telefono di origine e li invia all'API. Poiché il sistema incorporato conosce il numero di telefono, desidero inviare: {"srcPhone":12345678} anziché {"srcPhone":"http://host/phones/5"}. Ciò evita la necessità che il mio sistema embedded conosca la chiave primaria di ogni telefono (o GET Phones per numero ogni volta che desidera inviare una chiamata).

Google e i documenti di Django suggerivano che avrei potuto ottenere questo con chiavi naturali. Il mio tentativo segue:

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') 

T o test, creo un telefono con il numero 123456. Quindi I POST {"srcPhone": 123456} a http://host/calls/ (che è configurato in urls.py per eseguire la vista CallList). Questo dà un AttributeError a/calls/- l'oggetto 'int' non ha attributo 'startswith'. L'eccezione si verifica in rest_framework/relations.py (riga 355). Può pubblicare l'intera traccia se sarà utile. Leggendo Relations.py, sembra che REST Framework non stia cercando Telefoni per numero, ma elaborando l'attributo srcPhone come se fosse un URL. Questo sarebbe normalmente vero, ma voglio che cerchi Phones per chiave naturale, piuttosto che fornire l'URL. Cosa mi sono perso qui?

Grazie!

risposta

13

Quello che stai cercando è SlugRelatedField. Vedi docs here.

ma elaborare l'attributo srcPhone come se fosse un URL.

Esattamente. Stai utilizzando HyperlinkedModelSerializer, quindi la chiave srcPhone utilizza una relazione di collegamento ipertestuale per impostazione predefinita.

L'eccezione 'int' object has no attribute 'startswith' che stai vedendo è perché si aspetta una stringa URL, ma riceve un numero intero. In realtà ciò dovrebbe comportare un errore di convalida descrittivo, quindi ho created a ticket for that.

Se si utilizza invece un qualcosa di serializzatore come questo:

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

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

Allora la chiave 'srcPhone' sarà invece rappresentare il rapporto utilizzando il campo 'number' sul target del rapporto.

Sto pianificando di mettere un po 'più di lavoro per la documentazione di relazione a un certo punto presto, quindi spero che questo sarà più ovvio in futuro.

+0

Grazie per questo Tom, ora posso inviare chiamate solo con un numero intero di srcPhone. – EwanC

+0

Ho appena trovato questa risposta e risolto il mio problema! Molto bello spiegato – zeroliu

1

(Non puoi inviare questo come un commento, troppo a lungo)

risposta di Tom sopra risolto il problema.

Tuttavia, desidero anche avere un campo con collegamento ipertestuale alla risorsa Telefono. Lo SlugRelatedField mi consente di inviare un campo intero appartenente al telefono, ma quando si ottiene la risorsa chiamata risultante, viene serializzato come un numero intero. Sono sicuro che questa è una funzionalità voluta (non sembra molto elegante averla serializzare da un numero intero, ma con un collegamento ipertestuale). La soluzione che ho trovato è stata quella di aggiungere un altro campo al CallSerializer: src = serializers.HyperlinkedRelatedField(view_name='phone-detail',source='srcPhone',blank=True,read_only=True) e aggiungere quel campo alla classe Meta. Quindi I POST solo srcPhone (un numero intero) e GET srcPhone plus src, che è un collegamento ipertestuale alla risorsa Telefono.

+0

Non sono sicuro che tu abbia bisogno di 'blank = True', ma altrimenti, sì, sembra buono. –

Problemi correlati