Introduction:
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.
Key features of ModelViewSet:
-
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]
How can you customize your 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:
Standard Attributes:
- queryset: The queryset that should be used for returning objects from this view.
- serializer_class: Specifies the serializer class to use for validating and deserializing input, and for serializing output.
- lookup_field: The model field that should be used to lookup objects. Defaults to 'pk'.
- pagination_class: Pagination class to be used to paginate the list of objects.
Standard Methods:
- get_queryset(): Returns the queryset that will be used to retrieve objects.
- get_serializer_class(): Returns the class to use for the serializer.
- get_serializer(): Returns the serializer instance that should be used for validating and deserializing input.
- get_object(): Returns the object that this view is displaying.
- get_serializer_context(): Can be overridden to change the context dictionary passed to the serializer instance.
- filter_queryset(queryset): Given a queryset, filter it based on the view’s filtering configuration.
- paginate_queryset(queryset): Paginate the given queryset, and return a page object.
- get_pagination_response(data): Return a paginated style response for the given output data.
- get_view_name(): Returns the view name that should be used as the title of the view.
- get_view_description(): Returns the description to be used by browsable APIs and other descriptions.
- get_renderer_context(): Creates a context dictionary for rendering.
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?
- Separation of Concerns: Different parts of an application may require different subsets of data. Multiple serializers allow you to fetch only the necessary data for each context, separating concerns.
- Efficient Data Handling: By defining serializers specifically tailored to different API endpoints or logic, you can optimize data serialization/deserialization, improving performance and efficiency.
- Security: Having different serializers lets you expose only the necessary fields to users who do not need full access to the data. This minimizes exposure to sensitive information.
- Flexibility: Some API endpoints might need to validate data differently or handle it in a specific manner. Custom serializers provide the flexibility to add custom validation logic as needed.
- Maintainability: Breaking down complex functionalities into simpler, smaller serializers makes the codebase easier to maintain. Each serializer handles a well-defined responsibility.
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",
]
-
Link:
- ModelViewSets
- GenericViewSet
- GitHub repository: SoftDesk (Project 10 OpenClassrooms Path - SoftDesk -- create a secure RESTful API using Django REST framework)