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.
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.
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.
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
Different serializers for different methods within a viewset
override get_serializer_class recommended
Add fields on the fly that aren't present on the model
Using queryset annotation
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
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
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).
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.
Unique together with a user that is not set in the form
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()
# 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