djangoapidjango-rest-frameworkhypermedia

How do I get a custom hyperlinked field to include extra url variable and work for all ModelViewSets?


I use a variable in the base of my API url, identical to the setup found in the docs for Django REST Framework:

/api/<brand>/states/<state_pk>/

Everything after the base brand slug is a standard API format, and so I use ModelViewSets to generate all my list and detail views for my objects. Everything in the API is filtered by the brand, so this setup makes sense.

simplified project/urls.py

urlpatterns = patterns(
    '',
    url(r'^v2/(?P<brand_slug>\w+)/', include(router.urls, namespace='v2')),
)

simplified api/urls.py

router = routers.DefaultRouter()
router.register(r'states', StateViewSet)
router.register(r'cities', CityViewSet)

I also need hypermedia links for all models, and this is where I've run into problems. The REST framework doesn't know how to grab this brand variable and use it to generate correct links. Attempting to solve this problem by following the docs leaves me with 2 setbacks:

  1. While the docs explain how to overwrite the HyperlinkRelatedField class, they never say where to put THAT class so that it works with my Serializers.
  2. There's no mention on how to actually get the brand variable from the URL into the HyperlinkRelatedField class.

What are the missing elements here?


Solution

  • So, I figured it out.

    Getting the URL variable into the Serializer

    To do this, you need to overwrite the get_serializer_context() method for your ModelViewSet, and send in the variable from your kwargs

    class BrandedViewSet(viewsets.ModelViewSet):
        def get_serializer_context(self):
            context = super().get_serializer_context()
            context['brand_slug'] = self.kwargs.get('brand_slug')
            return context
    

    Then, you can just extend all of your ModelViewSets with that class:

    class StateViewSet(BrandedViewSet):
        queryset = State.objects.all()
        serializer_class = StateSerializer
    

    What's nice is that even though you've injected the Serializer with this variable, it's ALSO accessible from the HyperlinkedRelatedField class, via self.context, and that's how the next part is possible.

    Building a Custom Hypermedia link with extra URL variables

    The docs were correct in overwriting get_url():

    class BrandedHyperlinkMixin(object):
        def get_url(self, obj, view_name, request, format):
            """ Extract brand from url
            """
            if hasattr(obj, 'pk') and obj.pk is None:
                return None
    
            lookup_value = getattr(obj, self.lookup_field)
            kwargs = {self.lookup_url_kwarg: lookup_value}
            kwargs['brand_slug'] = self.context['brand_slug']
            return reverse(
                view_name, kwargs=kwargs, request=request, format=format)
    

    Except, you'll notice I'm grabbing the variable from the context I set in part 1. I was unable to get the context from the object as the docs suggested, and this method turned out to be simpler.

    The reason it's a mixin is because we need to extend TWO classes for this to work on all the url hyperlinks and not just the related field hyperlinks.

    class BrandedHyperlinkedIdentityField(BrandedHyperlinkMixin,
                                          serializers.HyperlinkedIdentityField):
        pass
    
    
    class BrandedHyperlinkedRelatedField(BrandedHyperlinkMixin,
                                         serializers.HyperlinkedRelatedField):
        pass
    
    
    class BrandedSerializer(serializers.HyperlinkedModelSerializer):
        serializer_related_field = BrandedHyperlinkedRelatedField
        serializer_url_field = BrandedHyperlinkedIdentityField
    

    Now we can safely extend our serializer and the hyperlinks show the brand variable!

    class StateSerializer(BrandedSerializer):
        class Meta:
            model = State
            fields = ('url', 'slug', 'name', 'abbrev', )