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
andget_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_classes
andauthentication_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
Last updated
Was this helpful?