Django Rest Framework
A first look at
defining api
Introduction
Welcome to the third episode of the series. If you missed the second part, you could read it here. Let’s write some code.
How to follow
You should check out the code here as this blog post omits a lot of details.
Requirements
A Dog can have multiple DogHouses.
A DogHouse corresponds to only one Dog.
A DogHouse can have multiple Colors.
Creating a Django App
Django automatically creates the main app with the same name as the project.
We can create multiple apps which separate groups of functionalities by execute
poetry run python manage.py startapp <app_name>
Replace <app_name> with house.
Database Design (Model)
from django.db import models
class Dog(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100)
age = models.IntegerField(null=False)
class Color(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100)
class DogHouse(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField(max_length=100)
dog = models.ForeignKey(Dog, on_delete=models.CASCADE)
colors = models.ManyToManyField(Color, related_name=‘ dog_house_color’, null=True)
Note
id will be generated automatically.
dog in the DogHouse model is a foreign key.
colors of the model is used to generated the intermediate table which record the ManyToMany relationship.
A serializer for Dog
Let’s take a look at a serializer for the Dog model. The convention is that we store serializers in serializers.py.
class DogSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False, read_only=True)
name = serializers.CharField(max_length=100, required=False)
age = serializers.IntegerField(required=False)
class Meta:
model = Dog
fields = ‘ __all__’
def create(self, validated_data):
validated_data.pop(‘ id’, None)
instance = Dog.objects.create(**validated_data)
instance.save()
return instance
def update(self, instance, validated_data):
instance.name = validated_data.get(‘ name’, instance.name)
instance.age = validated_data.get(‘ age’, instance.age)
instance.save()
return instance
The function created is be called when we are about to created (insert) a row. We also need to pop the id field out to prevent the id passed from users.
The update is called when we try to update a model (row).
Permission for Dog Api
An example of permissions.py. We can define the custom logic inside has_permissionn. In this case, we intend to use & (AND) operator with IsAuthenticated which we’ll see in the next section.
from rest_framework import permissions
class DogAccessPermission(permissions.BasePermission):
def has_permission(self, request, view):
# We can define custom permission logic here.
# Example : The user is in a particular group.
# Example : The dog’s age does not exceed 12.
# Example : The username of the requesting user is ‘ user1’.
# if request.user.username == ‘ user1’:
# return True
# else:
# return False
# To keep things simple, I would just return true.
return True
Defining the dog_api function
In views.py we define the function dog_api to handle the pattern api/dog, api/dog/ and api/dog/<dog_id>.
@api_view((‘ GET’, ‘ POST’, ‘ PUT’, ‘ DELETE’))
@parser_classes((JSONParser, MultiPartParser))
@permission_classes((IsAuthenticated & DogAccessPermission,))
@authentication_classes((TokenAuthentication,))
def dog_api(request, dog_id=0):
Model = Dog
Serializer = DogSerializer
if request.method == “ GET”:
if dog_id == 0:
objects = Model.objects.all()
serializer = Serializer(objects, many=True)
return JsonResponse(serializer.data, safe=False) # Why safe=False
else:
id = dog_id
object = Model.objects.filter(Q(id=id)).first()
serializer = Serializer(object)
return JsonResponse(serializer.data, safe=False)
elif request.method == “ POST”:
data = request.data.dict()
serializer = Serializer(data=data)
success = True
if serializer.is_valid():
try:
with transaction.atomic():
instance = serializer.save()
except DatabaseError:
success = False
else:
success = False
if success:
serializer = Serializer(instance=instance)
return JsonResponse(serializer.data, safe=False)
elif request.method == “ PUT”:
id = dog_id
object = Model.objects.filter(id=id).first()
if object is None:
return JsonResponse({‘ detail’ : “ Failed to update.”}, safe=False, status=http.HTTPStatus.INTERNAL_SERVER_ERROR)
data = request.data.dict()
serializer = Serializer(instance=object, data=data)
success = True
if serializer.is_valid():
try:
with transaction.atomic():
serializer.save()
except DatabaseError:
success = False
else:
success = False
if success:
return JsonResponse({‘ detail’: ‘ Updated successfully.’}, safe=False)
else:
return JsonResponse({‘ detail’ : “ Failed to update.”}, safe=False, status=http.HTTPStatus.INTERNAL_SERVER_ERROR)
Decorators
@api_view((‘ GET’, ‘ POST’, ‘ PUT’, ‘ DELETE’))
@parser_classes((JSONParser, MultiPartParser))
@permission_classes((IsAuthenticated & DogAccessPermission,))
@authentication_classes((TokenAuthentication,))
def dog_api(request, dog_id=0):
# function body
Note that the decorator ordering does matter (this is true for all Python decorators). The order of execution is queue from the bottom.
@authentication_classes must be at the bottom (executed first). The reason is that if we cannot authenticate the request, we should outright reject the request.
@api_view must be at the top. Other decorators must come after (below) @api_view.
@api_view specifies allowed request methods.
@permission_classes((IsAuthenticated & DogAccessPermission)) defines the permission composition. We can mix permissions with several custom ones. & (AND), | (OR) and ~ (NOT) are the allowed operators.
@authentication_classes((TokenAuthentication,)) specifies authentication scheme.
GET method
def dog_api(request, dog_id=0):
Model = Dog
Serializer = DogSerializer
if request.method == “ GET”:
if dog_id == 0:
objects = Model.objects.all()
serializer = Serializer(objects, many=True)
return JsonResponse(serializer.data, safe=False)
else:
id = dog_id
object = Model.objects.filter(Q(id=id)).first()
serializer = Serializer(object)
return JsonResponse(serializer.data, safe=False)
If dog_id is not present, dog_id takes 0 as the default value. We want dog_id = 0 to signal multiple instances of Dog.
Otherwise, the request wants a particular dog.
Note that the variable object can be None (the dog does not exist). In that case, the JsonResponse would return Dog with id = null. In the frontend, we can check for existence by checking if id is null or not.
POST and PUT Methods
elif request.method == “ POST”:
data = request.data.dict()
serializer = Serializer(data=data)
success = True
if serializer.is_valid():
try:
with transaction.atomic():
instance = serializer.save()
except DatabaseError:
success = False
else:
success = False
if success:
serializer = Serializer(instance=instance)
return JsonResponse(serializer.data, safe=False)
elif request.method == “ PUT”:
id = dog_id
object = Model.objects.filter(id=id).first()
if object is None:
return JsonResponse({‘ detail’ : “ Failed to update.”}, safe=False, status=http.HTTPStatus.INTERNAL_SERVER_ERROR)
data = request.data.dict()
serializer = Serializer(instance=object, data=data)
success = True
if serializer.is_valid():
try:
with transaction.atomic():
serializer.save()
except DatabaseError:
success = False
else:
success = False
if success:
return JsonResponse({‘ detail’: ‘ Updated successfully.’}, safe=False)
else:
return JsonResponse({‘ detail’ : “ Failed to update.”}, safe=False, status=http.HTTPStatus.INTERNAL_SERVER_ERROR)
How does the serializer know whether we are creating (INSERT) or updating (UPDATE)?
Compare PUT and POST, if we pass a variable to the instance argument of the serializer, then the serializer knows that we are updating.What is transaction.atomic?
We what database operations to execute successfully all at once. If there is an error, all operations will be roll-backed.
Outro
Oh! I think it’s time to go. See you next time!!