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):
serializer.save(**self.get_filter_dict())
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.