A ModelViewSet
in Django REST Framework (DRF) is a powerful view class that provides a set of common actions for working with models through an API. It combines the functionality of Django's generic views and viewsets, enabling you to quickly build CRUD (Create, Read, Update, Delete) APIs for your models without needing to manually define each individual action.
Pre-built CRUD actions: It automatically provides the following actions:
list
- GET
: Retrieves multiple objects of model instances.retrieve
- GET
: Retrieves a single model instance.create
- POST
: Creates a new model instance.update
- PUT
: Updates an existing model instance.partial_update
- PATCH
: Partially updates an existing model instance.destroy
- DELETE
: Deletes a model instance.Automatic routing: When you use ModelViewSet
with DRF's routers, URL routes are automatically generated for these actions.
Flexibility: You can easily customize or extend the default behavior by overriding methods or adding custom actions.
The queryset
and serializer_class
attributes are essential for using a ModelViewSet.
Here is a simple example of a ModelViewSet
from the DRF documentation:
# app/views.py
class AccountViewSet(viewsets.ModelViewSet):
queryset = Account.objects.all()
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
ModelViewSet
and what do you need to be aware of?You can use any of the standard attributes or method overrides that are provided by the GenericAPIView
.
Here is a list of some of the key ones:
1.) The get_queryset()
method is overridden by the DRF documentation in this example.
# app/views.py
class AccountViewSet(viewsets.ModelViewSet):
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
def get_queryset(self):
return self.request.user.accounts.all()
The DRF documentation shows this example because if you override the get_queryset()
method, you need to be aware that the basename
in the router registry has to be set to a value. Normally DRF will automatically create the basename
and set the value. It takes the values from the model name and creates the basename
with the model name in lower case. In this case, you must set the basename
to a value.
# project/urls.py
from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'accounts', AccountViewSet, basename='account')
urlpatterns = router.urls
In the above example:
# app/views.py
class AccountViewSet(viewsets.ModelViewSet):
queryset = Account.objects.all()
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
the basename
is automatically set to basename='account'
. It takes the model name of the queryset
attribute Account
and create a lower case name.
2.) Having multiple serializer classes:
Using a mixin is one way of generating multiple serializer classes. Why do I want to create multiple serializers?
The serializer mixin can look like this:
# mixins.py
class SerializerClassMixin:
serializer_create_class = None
serializer_detail_class = None
serializer_list_class = None
def get_serializer_class(self):
if self.action == "create":
return self.serializer_create_class
elif self.action == "retrieve":
return self.serializer_detail_class
elif self.action == "list":
return self.serializer_list_class
return super().get_serializer_class()
In a Django REST framework project, the self.action
is an attribute that represents the current action being performed by a viewset. It is automatically set by Django REST framework based on the HTTP method of the request and the viewset's configuration.
This Mixin is designed to allow different serializers to be used in different situations within a single viewset:
serializer_create_class: Used when the create action is being performed.
serializer_detail_class: Used for the retrieve action, accessing a single instance.
serializer_list_class: Used when listing a collection of instances with the list action.
By using self.action
, the get_serializer_class()
method dynamically chooses which serializer class to use based on the current action. This can be very helpful in a scenario where you need different serializers for different operations on the same resource.
Here is an example of a view:
# views.py
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from api.models.project import Project
from api.permissions import IsAuthor
from api.serializers.project import (
ProjectCreateSerializer,
ProjectListSerializer,
ProjectDetailSerializer,
)
from api.views.mixins import SerializerClassMixin
class ProjectViewSet(SerializerClassMixin, ModelViewSet):
serializer_class = ProjectCreateSerializer
serializer_create_class = ProjectCreateSerializer
serializer_detail_class = ProjectDetailSerializer
serializer_list_class = ProjectListSerializer
permission_classes = [IsAuthor, IsAuthenticated]
_project = None
@property
def project(self):
if self._project is None:
self._project = Project.objects.filter(contributors=self.request.user)
return self._project
def get_queryset(self):
# use order_by to avoid the warning for the pagination
return self.project.order_by("created_time")
def perform_create(self, serializer):
# save the author as author and as contributor (request.user)
serializer.save(author=self.request.user, contributors=[self.request.user])
In the ProjectViewSet
we integrate the serializer mixin (SerializerClassMixin
) to be able to use all the different serializers to list, create and display a detailed serializer.
We need to specify the basename
in the URL pattern because we have overridden the get_queryset()
method.
# urls.py
from rest_framework_nested import routers
from api.views.project import ProjectViewSet
app_name = "api"
router = routers.DefaultRouter()
router.register(r"projects", ProjectViewSet, basename="project")
Finally, there are the different serializers:
# serializers.py
from rest_framework import serializers
from api.models.project import Project
class ProjectCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = [
"id",
"name",
"description",
"project_type",
]
def validate(self, attrs):
if (
self.context["view"]
.project.filter(name=attrs["name"], project_type=attrs["project_type"])
.exists()
):
raise serializers.ValidationError("Attention! This project exists already.")
return attrs
class ProjectListSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = [
"id",
"name",
"author",
"contributors",
]
class ProjectDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = [
"id",
"created_time",
"name",
"description",
"project_type",
"author",
"contributors",
]