Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST framework - multiple lookup fields?

I have a model that more or less looks like this:

class Starship(models.Model):
    id = models.UUIDField(default=uuid4, editable=False, primary_key=True)
    name = models.CharField(max_length=128)
    hull_no = models.CharField(max_length=12, unique=True)

I have an unremarkable StarshipDetailSerialiser and StarshipListSerialiser (I want to eventually show different fields but for now they're identical), both subclassing serializers.ModelSerializer. It has a HyperlinkedIdentityField that refers back to the (UU)ID, using a home-brew class very similar to the original HyperlinkedIdentityField but with capability to normalise and handle UUIDs:

class StarshipListSerializer(HyperlinkedModelSerializer):
uri = UUIDHyperlinkedIdentityField(view_name='starships:starship-detail', format='html')

    class Meta:
         model = Starship
         fields = ('uri', 'name', 'hull_no')

Finally, there's a list view (a ListAPIView) and a detail view that looks like this:

class StarshipDetail(APIView):
    """
    Retrieves a single starship by UUID primary key.
    """

    def get_object(self, pk):
        try:
            return Starship.objects.get(pk=pk)
        except Starship.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        vessel = self.get_object(pk)
        serializer = StarshipDetailSerialiser(vessel, context={'request': request})
        return Response(serializer.data)

The detail view's URL schema is currently invoking the view based on the UUID:

...
url(r'vessels/id/(?P<pk>[0-9A-Fa-f\-]+)/$', StarshipDetail.as_view(), name='starship-detail'),
...

I now want users to be able to navigate and find the same vessel not just by UUID but also by their hull number, so that e.g. vessels/id/abcde1345...and so on.../ and vessels/hull/H1025/ would be able to resolve to the same entity. And ideally, regardless of whether one arrived at the detail view from ID or hull number, the serialiser, which is used with slight alterations in lists as well, should be able to have the ID hyperlinked to the ID-based link and the hull hyperlinked to a hull number based link (vessels/hull/H1025/). Is this at all possible? And if so, how would I go about it?

like image 337
Chris vCB Avatar asked Aug 07 '17 22:08

Chris vCB


1 Answers

1. Add the new routes

# in urls.py

urlpatterns = [
    ...,
    url(r'vessels/id/(?P<pk>[0-9A-Fa-f\-]+)/$', StarshipDetail.as_view(), name='starship-detail-pk'),
    url(r'vessels/hull/(?P<hull_no>[0-9A-Za-z]+)/$', StarshipDetail.as_view(), name='starship-detail-hull'),
]

Tweak the regex for hull_no as you want. Note that I gave distinct names to each route, starship-detail-pk and starship-detail-hull.

2. Add the hull field in the serializer

# in serializers.py

class StarshipListSerialiser(HyperlinkedModelSerializer):
    uri = UUIDHyperlinkedIdentityField(view_name='starship-detail-pk', format='html')
    hull_no = UUIDHyperlinkedIdentityField(view_name='starship-detail-hull', format='html', lookup_field='hull_no')

    class Meta:
         model = Starship
         fields = ('uri', 'name', 'hull_no')

3. Modify the view so it can also resolve objects based on hull

# in serializers.py

from django.shortcuts import get_object_or_404

from rest_framework.views import APIView, Response

from starwars.serializers import StarshipDetailSerialiser
from starwars.models import Starship


class StarshipDetail(APIView):

    def get(self, request, pk=None, hull_no=None, format=None):
        lookup = {'hull_no': hull_no} if pk is None else {'pk': pk}
        vessel = get_object_or_404(Starship, **lookup)
        serializer = StarshipDetailSerialiser(vessel, context={'request': request})
        return Response(serializer.data)

That should be enough to get you going with the detail view:

drf screencap

As a final note, you should be aware that it's not RESTful for the same resource to be available at two different URLs like this. Perhaps, as an alternate design decision, you might like to consider just defining the "one true route" for a resource, and adding in a "convenience" redirect from the other locator to the canonical URL.

like image 100
wim Avatar answered Oct 20 '22 12:10

wim