DEV Community

DoriDoro
DoriDoro

Posted on

Django REST Framework (DRF) - ModelViewSets

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]
Enter fullscreen mode Exit fullscreen mode

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:

  1. queryset: The queryset that should be used for returning objects from this view.
  2. serializer_class: Specifies the serializer class to use for validating and deserializing input, and for serializing output.
  3. lookup_field: The model field that should be used to lookup objects. Defaults to 'pk'.
  4. pagination_class: Pagination class to be used to paginate the list of objects.

Standard Methods:

  1. get_queryset(): Returns the queryset that will be used to retrieve objects.
  2. get_serializer_class(): Returns the class to use for the serializer.
  3. get_serializer(): Returns the serializer instance that should be used for validating and deserializing input.
  4. get_object(): Returns the object that this view is displaying.
  5. get_serializer_context(): Can be overridden to change the context dictionary passed to the serializer instance.
  6. filter_queryset(queryset): Given a queryset, filter it based on the view’s filtering configuration.
  7. paginate_queryset(queryset): Paginate the given queryset, and return a page object.
  8. get_pagination_response(data): Return a paginated style response for the given output data.
  9. get_view_name(): Returns the view name that should be used as the title of the view.
  10. get_view_description(): Returns the description to be used by browsable APIs and other descriptions.
  11. 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In the above example:

# app/views.py

class AccountViewSet(viewsets.ModelViewSet):
    queryset = Account.objects.all()
    serializer_class = AccountSerializer
    permission_classes = [IsAccountAdminOrReadOnly]
Enter fullscreen mode Exit fullscreen mode

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?

  1. 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.
  2. Efficient Data Handling: By defining serializers specifically tailored to different API endpoints or logic, you can optimize data serialization/deserialization, improving performance and efficiency.
  3. 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.
  4. 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.
  5. 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()
Enter fullscreen mode Exit fullscreen mode

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])
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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",
        ]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)