Populate parent field from url kwarg in a nested serializer in Django REST Framework

I have 2 models, Catalog and Epic:

class Catalog(models.Model):
    created_on = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(null=False, default=False)

class Epic(models.Model):
    name = models.CharField(max_length=128, null=False)
    slug = models.SlugField(null=False)
    catalog = models.ForeignKey(Catalog, null=False, on_delete=models.CASCADE)

I have created CRUD viewset and serializer for Catalog, using DRF. Now I want to create CRUD viewset and serializer for Epic, to be used like /catalogs/<int:catalog_pk>/epics/<int:pk>. I am using DRF-nested-router:

router = routers.SimpleRouter()
router.register("catalog", CatalogViewSet)

catalog_router = routers.NestedSimpleRouter(router, "catalog", lookup="catalog")
catalog_router.register("epics", EpicsViewSet, basename="epics")

urlpatterns = router.urls + catalog_router.urls

And these are my viewset and serializer for Epic:

class EpicsViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
    serializer_class = EpicSerializer
    permission_classes = (IsAuthenticated, CatalogPermissions)

    def get_queryset(self):
        return Epic.objects.filter(catalog=self.kwargs["catalog_pk"])

class EpicSerializer(serializers.ModelSerializer):

    class Meta:
        model = Epic
        fields = "__all__"

I am trying to run the creation endpoint like this: POST /catalog/1/epics/ with epic data, but I get the error that catalog field is missing in the payload. I want this to be done automatically from URL kwargs. I want it to take the catalog_id from kwargs and set the catalog instance to the newly created Epic instance in the serializer.

The starightforward way would be to override the create function in the serializer, but I am hesitant to do that and was wondering if there is a more "pythonic" way to do that.

>Solution :

You can work with a simple mixin that will patch both the get_queryset and the perform_create method:

class FilterCreateGetMixin:
    filter_field_name = None
    filter_kwarg_name = None

    def get_filter_dict(self):
        return {self.filter_field_name: self.kwargs[self.filter_kwarg_name]}

    def get_queryset(self, *args, **kwargs):
        return (
            super().get_queryset(*args, **kwargs).filter(**self.get_filter_dict())

    def perform_create(self, serializer):

then we can make a mixin, specifically for the category for example:

class CategoryFilterMixin(FilterCreateGetMixin):
    filter_field_name = 'catalog'
    filter_kwarg_name = 'catalog_pk'

and mix this in the viewset/API view:

class EpicSerializer(serializers.ModelSerializer):
    class Meta:
        model = Epic
        exclude = ['catalog']

class EpicsViewSet(
    CategoryFilterMixin, mixins.CreateModelMixin, viewsets.GenericViewSet
    serializer_class = EpicSerializer
    permission_classes = (IsAuthenticated, CatalogPermissions)

The advantage of this is that we can easily mix this into all other views where we have a category_pk in the path.

Leave a Reply