V článku si ukážeme ako pomocou Djanga respektíve Django Rest frameworku naprogramujeme jednoduché REST API s autentifikáciou a autorizáciou. Predpokladá sa, že programátor ovláda Django framework a tým pádom sa nemusíme zdržiavať vysvetlovaním základných nastavení projektu. Ako modelovú situáciu použijeme klasiku v podobe vytvorenia todo listu.

Závislosti

V prvom kroku si nainštalujeme Django REST framework, ktorý je nadstavbou Djanga a správa sa ako modul v projekte.

            INSTALLED_APPS = (
                ...
                'rest_framework',
            )
                            

Model TODO listu bude vyzerať takto:

            from django.db import models

            class Todo(models.Model):

                task = models.CharField(max_length=50)
                description = models.CharField(max_length=255, default=“")
                priority = models.SmallIntegerField(default=1)
                            

Spustíme vytvorenie migracie a samotnú migráciu.

            python manage.py makemigrations
            python manage.py migrate
                            

Serializácia modelov

Vytvoríme si serializer, ktorý má na starosti serializovať a deserializovať inštanciu našeho modelu do JSON tvaru. Vytváranie serializera je niečo podobné, ako práca s Django formulárom. Uložíme si súbor serializers.py. Nepoužijeme defaultný serializers.Serializer pretože by sme museli každú položku modulu nastavovať typom

            description = serializers.CharField(
                                required=False, allow_blank=True, max_length=255
                            )
                            

Miesto toho využijeme serializers.ModelSerializer. Modelový serializer definuje položky podľa modelu a my si tak uľahčíme robotu.

            class SnippetSerializer(serializers.ModelSerializer):
                class Meta:
                    model = Todo
                    fields = ('id', ’task', ‘description', ‘priority',)
                            

Do našeho modulu Todo pridáme urls.py súbor

            from django.urls import path
            from todo import views

            urlpatterns = [
                path(’todo/',views.TodoList.as_view()),
            ]
                            

a taktiež do hlavného urls.py v roote aplikácie link na naše API

            from django.conf import settings
            from django.conf.urls.static import static
            from django.urls import include
            from django.urls import path

            urlpatterns = [
                ...
                path('api/', include(’todo.urls')),
                ...
                            

Django Rest Framework umožňuje dvoma spôsobmi určiť, že view djanga je API views.

  • 1. pre view, ktoré je definované ako funkcia použijeme @api_view
  • 2. pre view, ktoré je definované ako classa použijeme APIView

Každý používa iný spôsob. Ja preferujem base-classed view. Umožňuje mi tak dodržiavať princíp DRY (dont repeat yourself) . Tu je náš kód vo views.py

            from todo.models import Todo
            from todo.serializers import TodoSerializer
            from django.http import Http404
            from rest_framework.views import APIView
            from rest_framework.response import Response
            from rest_framework import status


            class TodoList(APIView):
                """
                List all todos, or create a new todo.
                """
                def get(self, request, format=None):
                    todos = Todo.objects.all()
                    serializer = TodoSerializer(todos, many=True)
                    return Response(serializer.data)

                def post(self, request, format=None):
                    serializer = TodoSerializer(data=request.data)
                    if serializer.is_valid():
                        serializer.save()
                        return Response(serializer.data, status=status.HTTP_201_CREATED)
                    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
                            

Uvedeným spôsobom máme pripravené API, ktoré pri GET requeste zobrazí zoznam všetkých TODO úloh, ktoré sú v databáze. Pri validnom POST requeste vieme vložiť TODO úloh do databázy.

Na Djangu a jeho modulov je krásne, že obsahujú veľa “sugar” vecí, ktoré vývoj zjednodušujú. Kedže v jednoduchosti je krása, naše API vylepšíme použitím generických class-based view.

            from todo.models import Todo
            from todo.serializers import TodoSerializer
            from rest_framework import generics


            class TodoList(generics.ListCreateAPIView):
                queryset = Todo.objects.all()
                serializer_class = TodoSerializer
                            

Pár riadkov kódu a dosiahli sme rovnaký efekt, ako pri dedení view z triedy APIView. Generická trieda LIstCreateAPIView obsahuje v svojom vnútri metódy na ukladanie modelového objektu a tiež ponúka vylistovanie už vložených objektov. Pred pripravených generických view je v module rest_framework.generics veľa. Stačí si len vybrať. Ak by sme chceli API len s vylistovaním objektov, stačilo by dediť od ListAPIView.

Vlastná funkcionalita ukladania dát sa zabezpečí prepísaním metódy create vo views.

            …
            class TodoList(generics.ListCreateAPIView):
                queryset = Todo.objects.all()
                serializer_class = TodoSerializer

                create(self, validation_data):
                    # pristup k datam
                    print(validated_data[’task’])
                    # samotne ulozenie dat, ktore vrati model
                    obj = Todo.objects.create(**validated_data)
                    obj.save(foo=validated_data['task'])
                    return obj
                            

Vytvorené API umožňuje využívať funkcionalitu všetkým aj prihláseným aj neprihláseným používateľom. Ukážeme si, ako implementovať autentifikáciu a autorizáciu.

Do modelu TODO pridáme nasledovné

            …
            owner = models.ForeignKey('auth.User', related_name=‘todos', on_delete=models.CASCADE)
            …
                            

Taktiež si vytvoríme serializer na prácu s modelom User, ktorý si prevezmeme z už hotového riešenia modulu django.contrib.auth.model

            from django.contrib.auth.models import User

            class UserSerializer(serializers.ModelSerializer):
                todos = serializers.PrimaryKeyRelatedField(many=True, queryset=Todo.objects.all())

                class Meta:
                    model = User
                    fields = ('id', 'username', ’todos')
                            

Pridaním owner atribútu sme dosiahli reverzný vzťah modelových objekto User vs Todo. Dôsledkom toho je, že naše TODO obsahuje kľuč na vlastníka a vlastník zase reláciu na všetky vytvorené objekty Todo.

Práca s Userom

do views.py

            from django.contrib.auth.models import User

            class UserList(generics.ListAPIView):
                queryset = User.objects.all()
                serializer_class = UserSerializer


            class UserDetail(generics.RetrieveAPIView):
                queryset = User.objects.all()
                serializer_class = UserSerializer
                            

do urls.py

            url(r'^users/$', views.UserList.as_view()),
            url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
                            

Pre dosiahnutie automatického uloženia vlastníka vytvoreného objektu. Todo je potrebné prepísať metódu perform_create v views.py a tiež pridať do serializera atribút owner.
serializers.py

            ...
            owner = serializers.ReadOnlyField(source='owner.username’)
            ...
                            

views.py

            ...
            def perform_create(self, serializer):
                serializer.save(owner=self.request.user)
                            

Pri ukladaní Todo modelu sa nám z requestu prenesie prihlásený používateľ a nastaví sa ako vlastník vytváraného objektu. V serializeri sme definovali ReadOnlyField, čo znamená, že tento atribút nevieme priamo v API prepísať. Automaticky sa nastaví až v procese ukladania v metóde perform_create.

Máme prepojený Todo model s Userom a chceme aby vytvorené Todo bolo možné editovať len používateľom, ktorý ho vytvoril. Framework ponúka rôzne druhy autorizačných tried. My použijeme IsAuthenticatedOrReadOnly a tiež si vytvoríme jednu vlastnú. Prvá nám zabezpečí, aby vytvorene objekty videli len prihlásený používateľ. Tú druhú si zostrojíme tak, aby objekty mohol editovať len vlastník vytvoreného objektu.

            from rest_framework import permissions

            ...
            permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
                            

Ak by sme teraz skúsili vytvoriť objekt, nepodarí sa nám to. Je potrebné vytvoriť podstránku, ktorá umožní prihlásenie používateľa. Existuje však hotové riešenie a to v podobe frameworkovej internej url adresy

            urlpatterns += [
                url(r'^api-auth/', include('rest_framework.urls')),
            ]
                            

Vlastné riešenie autorizácie

            from rest_framework import permissions


            class IsOwnerOrReadOnly(permissions.BasePermission):
                """
                Custom permission to only allow owners of an object to edit it.
                """

                def has_object_permission(self, request, view, obj):
                    # Read permissions are allowed to any request,
                    # so we'll always allow GET, HEAD or OPTIONS requests.
                    if request.method in permissions.SAFE_METHODS:
                        return True

                    # Write permissions are only allowed to the owner of the snippet.
                    return obj.owner == request.user
                            

a upravíme naše views.py

            from todos.permissions import IsOwnerOrReadOnly
            ...
            permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,)
            ...
                            

Otestovanie

Dotaz bez autorizácie

            http POST http://127.0.0.1:8000/todos/ task="nejaka uloha"

            {
                "detail": "Authentication credentials were not provided."
            }
                            

Autentifikácia našeho requestu môže vyzerať aj takto

            http -a admin:password123 POST http://127.0.0.1:8000/todos/ task="nejaka uloha"

            {
                "id": 1,
                “task": "nejaka uloha",
                “description": "",
                “priority": 1,
            }
                            

Záver

Na jednoduchom príklade sme si ukázali použitie RESTového rozhrania v prostredí Django. Príklad ilustruje len základnú prácu s API. Do budúca plánujem uverejniť článok spojenia REST rozhrania s asynchrónnym task queue frameworkom Celary.


Michal Kalman
Michal Kalman
Softvérový vývojár

Full-stack developer a podnikateľ zameriavajúci sa na platformu Java, Python. Mám dlhoročné profesionálne skúsenosti s vývojom Java/JavaEE aplikácií. Som zakladateľ firmy Morione, ktorá sa venuje návrhom a realizáciou škálovateľných webových aplikácií. Podporujem a vyvíjam viaceré startupy. Vo voľnom čase cestujem po svete, behám po horách, píšem blogy a tvorím kreatívne videá.