2

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?

rgilligan
  • 754
  • 5
  • 18

1 Answers1

2

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', )
rgilligan
  • 754
  • 5
  • 18