docs
  • Overview
  • 🐍 PYTHON
    • Type Hints
    • PEP8 Style Guide for Python Code
    • 🏡Pipenv
    • Pathlib
  • 🕸Django
    • 🗄models
      • 🎯Best Practices
      • 🚦Django Signals
    • ⚙️ settings
    • DRF
      • Serializer
      • Authentication
      • Permissions
      • Viewsets
    • Testing
      • Faker and Factory Boy
    • 🧪Test Coverage
    • 💦Python-Decouple
    • Django Tips:
    • 💾Django ORM Queryset
    • Custom Exceptions
    • Celery
    • Resources
  • Deploy
    • 🚀Django Deployment
    • 🔒Setup SSL Certificate
  • 💾Database
    • MongoDB
  • 🛠️DevOps
    • 🖥Scripting
      • A First Script
      • Loops
      • Test
      • Variables
      • External programs
      • Functions
    • Command Line Shortcuts
    • Basic Linux Commands
    • 🎛Microservices
    • 🐳Docker
      • Docker Commands
      • Docker Compose
      • Django project
    • Kubernates
  • 📝Software IDE
    • EditorConfig
    • Linters
    • VsCode
Powered by GitBook
On this page
  • Viewsets In Django Rest Framework
  • Advantages of ViewSets
  • 1. ViewSet
  • 2. GenericViewSet
  • 3. ModelViewSet
  • Only show entities related to the current user
  • Different serializers for different methods within a viewset
  • Add fields on the fly that aren't present on the model
  • Using queryset annotation
  • Using method on a serializer
  • Pass additional data to the serializer
  • Use a serializer for the query parameters
  • Use string value in an API for a field that has more efficient DB representation (enum)
  • Unique together with a user that is not set in the form
  • Resources

Was this helpful?

  1. 🕸Django
  2. DRF

Viewsets

Viewsets in Django Rest Framework

Viewsets In Django Rest Framework

ViewSets are just a type of class based view but it do not provide request method handlers like "get()", "post()", "patch()", "delete()", etc. But, it provides actions such as "create()", "list()", "retrieve()", "update()", "partial_update()" and "destroy()". DRF allows us to combine the logic for a set of related views in a single class "ViewSet". ViewSets can speed-up the development and better maintainability. In class based views we map views to urls using url config, but where as in viewsets we use routers to register the viewsets. Routers simplifies the process of configuring the viewsets to urls.

Advantages of ViewSets

  • Repeated functionality can be combined into a single class.

  • Routers are used to wiring up the URL configurations so, we do not need to write URL configurations externally.

  • For large API's we can enforce a consistent URL configuration throughout the API. ViewSet classes in DRF

1. ViewSet

  • It does not provide any implementations of actions. We have to override the class and define the action implementations explicitly.

  • We can use the attributes like permission_classes, authentication_classes

  • Example: Let's define a simple viewsets that can be used to list or retrieve all the students in the school model.

# models.py

from django.db import models

class Student(models.Model):
    name = models.CharField(max_length=100)
    age = models.PositiveIntegerField()

    def __str__(self):
    return "{name}".format(name=self.name)
    
# serializers.py

from rest_framework import serializers
from .models import Student

class StudentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Student
        fields = ('id', 'name', 'age')
        
# views.py

from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from rest_framework.response import Response
from .models import Student
from .serializers import StudentSerializer

class StudentViewSet(viewsets.ViewSet):

    def list(self, request):
        queryset = Student.objects.all()
        serializer = StudentSerializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        queryset = Student.objects.all()
        user = get_object_or_404(queryset, pk=pk)
        serializer = StudentSerializer(user)
        return Response(serializer.data)

2. GenericViewSet

  • Just like "ViewSet", It also does not provide the implementation of actions.

  • We ca use the attributes like permission_classes, authentication_classes

  • The only difference between ViewSet and GenericViewSet is that it provides generic methods like get_object and get_queryset.

from rest_framework import viewsets
from rest_framework.response import Response
from .models import Student
from .serializers import StudentSerializer

class StudentGenericViewSet(viewsets.GenericViewSet):

    def get_queryset(self):
        queryset = Student.objects.all()
        return queryset
    
    def get_object(self):
    queryset = self.get_queryset()
    obj = queryset.get(pk=self.kwargs['pk'])
    return obj

    def list(self, request):
        queryset = self.get_queryset()
        serializer = StudentSerializer(queryset, many=True)
        return Response(serializer.data)
    
    def retrieve(self, request, **kwargs):
        obj = self.get_object()
        serializer = StudentSerializer(obj)
        return Response(serializer.data)

3. ModelViewSet

  • It provides all the actions by default (i.e .list(), .retrieve(), .create(), .update(), .partial_update(), and .destroy()).

  • Most of the time we use this because it provides generic functionality so, we simply need to override the attributes like queryset , serializer_class , permission_classesand authentication_classes.

  • If we have any conditional logic then we can override methods like get_object, get_queryset, get_permission_classes, etc.

from rest_framework import viewsets
from .models import Student
from .serializers import StudentSerializer

class StudentModelViewSet(viewsets.ModelViewSet):
    queryset = Student.objects.all()
    serializer_class = StudentSerializer
    

# urls.py

from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register('student-viewset', views.StudentViewSet, basename='student_vs')
router.register('student-generic-viewset', views.StudentGenericViewSet, basename='student_gvs')
router.register('student-model-viewset', views.StudentModelViewSet, basename='student_mvs')
urlpatterns = router.urls
# print(urlpatterns)

Only show entities related to the current user

Private date visible to the owning user

Implementation: The best place to filter the queryset is to override get_queryset method

class AccountsViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = AccountSerializer
    basename = "account"
    
    def get_queryset(self) -> Queryset[models.Account]:
        assert isinstance(self.request.user, User)
        queryset = Accounts.objects.filter((user=self.request.user)
        ....
        return queryset

Different serializers for different methods within a viewset

override get_serializer_class recommended

class AccountsViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = AccountSerializer
    basename = "account"

    def get_serializer_class(self):
        if self.action in ("create", "update"):
            return AccountEditSerializer
        if self.action == "retrieve":
            return AccountWithValuesSerializer

        return AccountSerializer

Add fields on the fly that aren't present on the model

Using queryset annotation

class AccountsViewSet(viewsets.ModelViewSet):
    permission_classes = [permissions.IsAuthenticated]
    serializer_class = AccountSerializer
    pagination_class = LimitOffsetPagination
    basename = "account"

    def get_queryset(self) -> QuerySet[models.Account]:
        assert isinstance(self.request.user, User)
        queryset = models.Account.objects.filter(user=self.request.user).annotate(
            positions_count=Count("positions", distinct=True),
            transactions_count=Count("positions__transactions", distinct=True),
        )
        return queryset
class AccountSerializer(serializers.ModelSerializer[Account]):

    positions_count = serializers.IntegerField()
    transactions_count = serializers.IntegerField()

    class Meta:
        model = Account
        fields = [
            "id",
            "nickname",
            "description",
            "balance",
            "last_modified",
            "positions_count",
            "transactions_count",
        ]

Using method on a serializer

There is an easy way with drf to do this with SerializerMethodField.

Add a field to serialized data that requires custom logic

How to do it"

  • define a field as serializers.SerializerMethodField()

  • add it to fields list in Meta

  • define get_myfieldname method

class AccountWithValuesSerializer(serializers.ModelSerializer[Account]):

    positions_count = serializers.IntegerField()
    transactions_count = serializers.IntegerField()
    currency = CurrencyField()
    values = serializers.SerializerMethodField()
    
    class Meta:
        model = Account
        fields = [
            "id",
            "currency",
            "nickname",
            "description",
            "balance",
            "last_modified",
            "positions_count",
            "transactions_count",
            "values",
        ]
    
    def get_values(self, obj):
        from_date = self.context["from_date"]
        to_date = self.context["to_date"]
        
        return obj.value_history_per_position(from_date, to_date")

Pass additional data to the serializer

  • Use additional data to generate and an additional field

  • Perform additional validation

override get_serializer_context , the data can come e.g. from self.request, self.request.user of self.request.query_params

def get_serializer_context(self) -> Dict[str, Any]:
    context: Dict[str, Any] = super().get_serializer_context()
    query = FromToDateSerializer(data=self.request.query_params)
    context["request"] = self.request
    return context
    

Use a serializer for the query parameters

Serializers transform data between formats such as JSON and native python, they also provide a good place to put your validation logic. we can use a serializer to extract and validate data from the query parameters (also known as URL params).

class FromToDatesSerializer(serializers.Serializer[Any]):
    from_date = serializers.DateField(required=False)
    to_date = serializers.DateField(required=False)
class AccountsViewSet(viewsets.ModelViewSet):
    ...
    def get_serializer_context(self) ->  Dict[str, Any]:
        context: Dict[str, Any] = super().get_serializer_context()
        query = FromToDatesSerializer(data=self.request.query_params)
        context["request"] = self.request

        if query.is_valid(raise_exception=True):
            data = query.validated_data
            self.query_data = data
            context["from_date"] = self.query_data.get(
                "from_date",
                datetime.date.today() - datetime.timedelta(days=30),
            )
            context["to_date"] = self.query_data.get("to_date", datetime.date.today())
        return context

Use string value in an API for a field that has more efficient DB representation (enum)

If a field can only have a limited number of options, enums are a great choice. Django provides TextChoices, IntegerChoices, and Choices to make it very easy. My preferred field is the IntegerChoices because it will end up using much less space in the database even if the represented value is a string.

class Currency(models.IntegerChoices):
    EUR = 1, _("EUR")
    GBP = 2, _("GBP")
    USD = 3, _("USD")
    GBX = 4, _("GBX")
    
    
def currency_enum_from_string(currency: str) -> Currency:
    try:
        return Currency[currency]
    except KeyError:
        raise ValueError("Unsupported currency '%s'" % currency)


def currency_string_from_enum(currency: Currency) -> str:
    return Currency(currency).label
    

class Account(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    currency = models.IntegerField(choices=Currency.choices, default=Currency.EUR)
    nickname = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    

class CurrencyField(serializers.IntegerField):
    def to_representation(self, value):
        return models.currency_string_from_enum(value)

    def to_internal_value(self, value):
        return models.currency_enum_from_string(value)
class AccountEditSerializer(serializers.ModelSerializer[Account]):

    # Currency needs to be changed from string to enum.
    currency = CurrencyField()

Unique together with a user that is not set in the form

# In the model.

    class Meta:
        unique_together = [["user", "nickname"]]

# In the serializers

    class Meta:
        model = Account
        fields = [
            "id",
            "currency",
            "nickname",
            "description",
        ]

    def validate_nickname(self, value):
        # If user was also included in the serializer then unique_together
        # constraint would be automatically evaluated, but
        # since user is not included in the serializer the validation is
        # done manually.
        request = self.context.get("request")
        if request and hasattr(request, "user"):
            user = request.user
            if Account.objects.filter(user=user, nickname=value).count() > 0:
                raise serializers.ValidationError(
                    f"User already has an account with name: '{value}'"
                )
        return value

Resources

  • Drf Recipes

PreviousPermissionsNextTesting

Last updated 3 years ago

Was this helpful?