Django Rest Framework

A first look at

defining api

Introduction

Wel­come to the third episode of the series. If you missed the sec­ond 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 mul­ti­ple Dog­Houses.
A Dog­House cor­re­sponds to only one Dog.
A Dog­House can have mul­ti­ple Colors.

Creating a Django App

Django auto­mat­i­cally cre­ates the main app with the same name as the project.
We can cre­ate mul­ti­ple apps which sep­a­rate groups of func­tion­al­i­ties by exe­cute

poetry run python man­age.py star­tapp <app_name>

Replace <app_name> with house.

Database Design (Model)

from django.db import mod­els

class Dog(mod­els.Model):
    id = mod­els.BigAuto­Field(pri­mary_key=True)
    name = mod­els.CharField(max_length=100)
    age = mod­els.Inte­ger­Field(null=False)

class Color(mod­els.Model):
    id = mod­els.BigAuto­Field(pri­mary_key=True)
    name = mod­els.CharField(max_length=100)

class Dog­House(mod­els.Model):
    id = mod­els.BigAuto­Field(pri­mary_key=True)
    name = mod­els.CharField(max_length=100)

    dog = mod­els.For­eignKey(Dog, on_delete=mod­els.CAS­CADE)
    col­ors = mod­els.Many­ToMany­Field(Color, related_name=dog_house_color’, null=True)

Note

  1. id will be gen­er­ated auto­mat­i­cally.

  2. dog in the Dog­House model is a for­eign key.

  3. col­ors of the model is used to gen­er­ated the inter­me­di­ate table which record the Many­ToMany rela­tion­ship.

A serializer for Dog

Let’s take a look at a seri­al­izer for the Dog model. The con­ven­tion is that we store seri­al­iz­ers in seri­al­iz­ers.py.

class DogSe­ri­al­izer(seri­al­iz­ers.Model­Seri­al­izer):
    id = seri­al­iz­ers.Inte­ger­Field(required=False, read_only=True)
    name = seri­al­iz­ers.CharField(max_length=100, required=False)
    age = seri­al­iz­ers.Inte­ger­Field(required=False)

    class Meta:
        model = Dog
        fields =__all__’

    def cre­ate(self, val­i­dated_data):

        val­i­dated_data.pop(id’, None)
        instance = Dog.objects.cre­ate(**val­i­dated_data)

        instance.save()
        return instance

    def update(self, instance, val­i­dated_data):

        instance.name = val­i­dated_data.get(name’, instance.name)
        instance.age = val­i­dated_data.get(age’, instance.age)

        instance.save()
        return instance

The func­tion cre­ated is be called when we are about to cre­ated (insert) a row. We also need to pop the id field out to pre­vent the id passed from users.

The update is called when we try to update a model (row).

Permission for Dog Api

An exam­ple of per­mis­sions.py. We can define the cus­tom logic inside has_per­mis­sionn. In this case, we intend to use & (AND) oper­a­tor with IsAuthen­ti­cated which we’ll see in the next sec­tion.


from rest_frame­work import per­mis­sions

class DogAc­cess­Per­mis­sion(per­mis­sions.BasePer­mis­sion):

    def has_per­mis­sion(self, request, view):
        # We can define cus­tom per­mis­sion logic here.
        # Exam­ple : The user is in a par­tic­u­lar group.
        # Exam­ple : The dog’s age does not exceed 12.

        # Exam­ple : The user­name of the request­ing user isuser1’.
        # if request.user.user­name ==user1’:
        #     return True
        # else:
        #     return False

                # To keep things sim­ple, I would just return true.
        return True

Defining the dog_api function

In views.py we define the func­tion dog_api to han­dle the pat­tern api/dog, api/dog/ and api/dog/<dog_id>.

@api_view((GET’,POST’,PUT’,DELETE’))
@parser_classes((JSON­Parser, Mul­ti­Part­Parser))
@per­mis­sion_classes((IsAuthen­ti­cated & DogAc­cess­Per­mis­sion,))
@authen­ti­ca­tion_classes((Token­Authen­ti­ca­tion,))
def dog_api(request, dog_id=0):

    Model = Dog
    Seri­al­izer = DogSe­ri­al­izer
    if request.method ==GET”:
        if dog_id == 0:
            objects = Model.objects.all()
            seri­al­izer = Seri­al­izer(objects, many=True)
            return Json­Re­sponse(seri­al­izer.data, safe=False) # Why safe=False

        else:
            id = dog_id
            object = Model.objects.fil­ter(Q(id=id)).first()
            seri­al­izer = Seri­al­izer(object)
            return Json­Re­sponse(seri­al­izer.data, safe=False)

    elif request.method ==POST”:
        data = request.data.dict()
        seri­al­izer = Seri­al­izer(data=data)
        suc­cess = True
        if seri­al­izer.is_valid():
            try:
                with trans­ac­tion.atomic():
                    instance = seri­al­izer.save()
            except Data­base­Error:
                suc­cess = False
        else:
            suc­cess = False
        if suc­cess:
            seri­al­izer = Seri­al­izer(instance=instance)
            return Json­Re­sponse(seri­al­izer.data, safe=False)

    elif request.method ==PUT”:
        id = dog_id
        object = Model.objects.fil­ter(id=id).first()

        if object is None:
            return Json­Re­sponse({detail’ :Failed to update.”}, safe=False, sta­tus=http.HTTP­Sta­tus.INTER­NAL_SERVER_ERROR)

        data = request.data.dict()
        seri­al­izer = Seri­al­izer(instance=object, data=data)

        suc­cess = True
        if seri­al­izer.is_valid():
            try:
                with trans­ac­tion.atomic():
                    seri­al­izer.save()
            except Data­base­Error:
                suc­cess = False
        else:
            suc­cess = False

        if suc­cess:
            return Json­Re­sponse({detail’:Updated suc­cess­fully.’}, safe=False)
        else:
            return Json­Re­sponse({detail’ :Failed to update.”}, safe=False, sta­tus=http.HTTP­Sta­tus.INTER­NAL_SERVER_ERROR)

Decorators

@api_view((GET’,POST’,PUT’,DELETE’))
@parser_classes((JSON­Parser, Mul­ti­Part­Parser))
@per­mis­sion_classes((IsAuthen­ti­cated & DogAc­cess­Per­mis­sion,))
@authen­ti­ca­tion_classes((Token­Authen­ti­ca­tion,))
def dog_api(request, dog_id=0):
        # func­tion body

Note that the dec­o­ra­tor order­ing does mat­ter (this is true for all Python dec­o­ra­tors). The order of exe­cu­tion is queue from the bot­tom.

@authen­ti­ca­tion_classes must be at the bot­tom (exe­cuted first). The rea­son is that if we can­not authen­ti­cate the request, we should out­right reject the request.

@api_view must be at the top. Other dec­o­ra­tors must come after (below) @api_view.

@api_view spec­i­fies allowed request meth­ods.

@per­mis­sion_classes((IsAuthen­ti­cated & DogAc­cess­Per­mis­sion)) defines the per­mis­sion com­po­si­tion. We can mix per­mis­sions with sev­eral cus­tom ones. & (AND), | (OR) and ~ (NOT) are the allowed oper­a­tors.

@authen­ti­ca­tion_classes((Token­Authen­ti­ca­tion,)) spec­i­fies authen­ti­ca­tion scheme.

GET method

def dog_api(request, dog_id=0):

    Model = Dog
    Seri­al­izer = DogSe­ri­al­izer
    if request.method ==GET”:
        if dog_id == 0:
            objects = Model.objects.all()
            seri­al­izer = Seri­al­izer(objects, many=True)
            return Json­Re­sponse(seri­al­izer.data, safe=False)

        else:
            id = dog_id
            object = Model.objects.fil­ter(Q(id=id)).first()
            seri­al­izer = Seri­al­izer(object)
            return Json­Re­sponse(seri­al­izer.data, safe=False)

If dog_id is not present, dog_id takes 0 as the default value. We want dog_id = 0 to sig­nal mul­ti­ple instances of Dog.
Oth­er­wise, the request wants a par­tic­u­lar dog.
Note that the vari­able object can be None (the dog does not exist). In that case, the Json­Re­sponse would return Dog with id = null. In the fron­tend, we can check for exis­tence by check­ing if id is null or not.

POST and PUT Methods

elif request.method ==POST”:
    data = request.data.dict()
    seri­al­izer = Seri­al­izer(data=data)
    suc­cess = True
    if seri­al­izer.is_valid():
        try:
            with trans­ac­tion.atomic():
                instance = seri­al­izer.save()
        except Data­base­Error:
            suc­cess = False
    else:
        suc­cess = False
    if suc­cess:
        seri­al­izer = Seri­al­izer(instance=instance)
        return Json­Re­sponse(seri­al­izer.data, safe=False)
elif request.method ==PUT”:
    id = dog_id
    object = Model.objects.fil­ter(id=id).first()

    if object is None:
        return Json­Re­sponse({detail’ :Failed to update.”}, safe=False, sta­tus=http.HTTP­Sta­tus.INTER­NAL_SERVER_ERROR)

    data = request.data.dict()
    seri­al­izer = Seri­al­izer(instance=object, data=data)

    suc­cess = True
    if seri­al­izer.is_valid():
        try:
            with trans­ac­tion.atomic():
                seri­al­izer.save()
        except Data­base­Error:
            suc­cess = False
    else:
        suc­cess = False

    if suc­cess:
        return Json­Re­sponse({detail’:Updated suc­cess­fully.’}, safe=False)
    else:
        return Json­Re­sponse({detail’ :Failed to update.”}, safe=False, sta­tus=http.HTTP­Sta­tus.INTER­NAL_SERVER_ERROR)

Outro

Oh! I think it’s time to go. See you next time!!