1 Django REST Framework PL
teacher edited this page 2026-04-13 15:12:35 +02:00

Django REST Framework — Kompletny przewodnik po zadaniach

Niniejszy przewodnik omawia wszystkie dziesięć zadań z katalogu django-assignments/restapi/. Każde zadanie stanowi samodzielny projekt Django, który wykorzystuje Django REST Framework (DRF) do udostępniania API w formacie JSON. Struktura projektu jest identyczna z wariantem klasycznym: katalog config/ zawiera ustawienia, natomiast app/ — kod aplikacji.

Charakterystyczną cechą tych zadań jest to, że klasy serializer i ViewSet są definiowane bezpośrednio w pliku app/views.py, a nie w osobnych plikach. Takie podejście jest celowe z dydaktycznego punktu widzenia: wszystko, co dotyczy jednego endpoint-u, jest widoczne na jednym ekranie.


Wymagania wstępne

Wersja Pythona

python --version   # 3.10 lub nowszy

Środowisko wirtualne i instalacja

python -m venv venv
source venv/bin/activate
pip install Django djangorestframework

Uruchamianie dowolnego zadania

python manage.py migrate
python manage.py runserver

Serwer nasłuchuje pod adresem http://127.0.0.1:8000. Otwarcie dowolnego endpoint-u DRF w przeglądarce wyświetla Browsable API — czytelny interfejs HTML do przeglądania i testowania API bez potrzeby używania Postmana.

Endpoint health

Każde zadanie udostępnia GET /, który zwraca:

{"framework": "django", "variant": "restapi", "status": "ok"}

Jest to szybka weryfikacja poprawnego działania serwera.


Styl kodu DRF stosowany w zadaniach

Wszystkie serializer-y i ViewSet-y są definiowane bezpośrednio w views.py. Przykład z zadania 01:

# Everything in one file: import → model → serializer → viewset
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from app.models import Todo

class TodoSerializer(ModelSerializer):
    class Meta:
        model  = Todo
        fields = ["id", "title", "description", "done", "created_at", "updated_at"]
        read_only_fields = ["id", "created_at", "updated_at"]

class TodoViewSet(ModelViewSet):
    queryset         = Todo.objects.all().order_by("-created_at")
    serializer_class = TodoSerializer

Następnie w urls.py:

from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register("todos", TodoViewSet, basename="todo")
urlpatterns = [path("api/", include(router.urls))]

DefaultRouter generuje sześć standardowych endpoint-ów z jednej rejestracji:

Metoda URL Akcja
GET /api/todos/ list
POST /api/todos/ create
GET /api/todos/{id}/ retrieve
PUT /api/todos/{id}/ update
PATCH /api/todos/{id}/ partial_update
DELETE /api/todos/{id}/ destroy

Zadanie 01: Podstawowe API listy zadań

Lokalizacja: django-assignments/restapi/01-basic-todo-api/

Czego się nauczysz

  • ModelSerializer i read_only_fields
  • ModelViewSet dla pełnego CRUD w minimalnej liczbie linii kodu
  • DefaultRouter do automatycznego generowania tras URL
  • Pole updated_at oraz auto_now=True

Kluczowe koncepcje

read_only_fields uniemożliwia klientom dostarczanie wartości dla pól zarządzanych przez system. Pole id jest przypisywane przez bazę danych; pola created_at i updated_at są ustawiane przez Django. Wymienienie ich jako read-only oznacza, że DRF ignoruje je podczas tworzenia i aktualizacji, nawet jeśli klient je prześle.

auto_now=True na polu updated_at aktualizuje znacznik czasu przy każdym wywołaniu save(). Jest śledzone oddzielnie od created_at (które używa auto_now_add=True), co pozwala wykryć, czy rekord był niedawno modyfikowany.

ModelViewSet udostępnia akcje list, create, retrieve, update, partial_update i destroy w jednej klasie. Każda akcja automatycznie wywołuje is_valid() i save() serializer-a. Nadpisanie jest możliwe na każdym poziomie: perform_create — aby dodać logikę przy tworzeniu, get_queryset — aby filtrować wyniki, update — dla niestandardowego zachowania zapisu.

Konfiguracja

pip install Django djangorestframework
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

# Create
curl -X POST http://127.0.0.1:8000/api/todos/ \
  -H "Content-Type: application/json" \
  -d '{"title": "Buy groceries", "description": "Milk and eggs"}'

# List
curl http://127.0.0.1:8000/api/todos/

# Partial update (mark done)
curl -X PATCH http://127.0.0.1:8000/api/todos/1/ \
  -H "Content-Type: application/json" \
  -d '{"done": true}'

# Delete
curl -X DELETE http://127.0.0.1:8000/api/todos/1/

Warto też otworzyć http://127.0.0.1:8000/api/todos/ w przeglądarce — DRF Browsable API pozwala wysyłać żądania POST bezpośrednio ze strony.


Zadanie 02: Blog osobisty

Lokalizacja: django-assignments/restapi/02-personal-blog/

Czego się nauczysz

  • lookup_field na ViewSet do wyszukiwania po innym polu niż klucz główny
  • Walidacja SlugField w serializer-ze
  • Routing URL ze slug-iem zamiast identyfikatora całkowitego

Kluczowe koncepcje

lookup_field = "slug" na ViewSet instruuje DRF, aby używał pola slug zamiast domyślnego pk w URL-ach szczegółowych. Router generuje /api/posts/{slug}/ zamiast /api/posts/{id}/. Nie jest wymagana żadna dodatkowa konfiguracja — DRF odczytuje ustawienie lookup_field i automatycznie dostosowuje routing oraz zapytania do bazy danych.

class PostViewSet(ModelViewSet):
    queryset         = Post.objects.all().order_by("-created_at")
    serializer_class = PostSerializer
    lookup_field     = "slug"

Ograniczenie unikalności w serializer-ze: SlugField(unique=True) na modelu tworzy ograniczenie w bazie danych. ModelSerializer DRF wykrywa to ograniczenie i automatycznie dodaje UniqueValidator do pola serializer-a. Próba utworzenia wpisu z duplikującym się slug-iem zwraca odpowiedź 400 Bad Request z opisowym komunikatem błędu.

Konfiguracja

pip install Django djangorestframework
python manage.py makemigrations
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

curl -X POST http://127.0.0.1:8000/api/posts/ \
  -H "Content-Type: application/json" \
  -d '{"title": "Hello World", "slug": "hello-world", "content": "First post."}'

# Retrieve by slug (not by id)
curl http://127.0.0.1:8000/api/posts/hello-world/

# Update
curl -X PATCH http://127.0.0.1:8000/api/posts/hello-world/ \
  -H "Content-Type: application/json" \
  -d '{"content": "Updated content."}'

# Duplicate slug → 400
curl -X POST http://127.0.0.1:8000/api/posts/ \
  -H "Content-Type: application/json" \
  -d '{"title": "Other", "slug": "hello-world", "content": "..."}'

Zadanie 03: Profile użytkowników i przesyłanie plików

Lokalizacja: django-assignments/restapi/03-user-profile-uploads/

Czego się nauczysz

  • MultiPartParser i FormParser na ViewSet
  • SerializerMethodField dla obliczanych pól w odpowiedzi
  • request.build_absolute_uri() do generowania pełnych adresów URL
  • Przekazywanie obiektu żądania przez kontekst serializer-a

Kluczowe koncepcje

SerializerMethodField dodaje pole tylko do odczytu, którego wartość jest obliczana przez metodę serializer-a:

class ProfileSerializer(ModelSerializer):
    avatar_url = SerializerMethodField()

    class Meta:
        model  = Profile
        fields = ["id", "display_name", "bio", "avatar", "avatar_url", "created_at"]
        read_only_fields = ["id", "created_at", "avatar_url"]

    def get_avatar_url(self, obj):
        request = self.context.get("request")
        if not obj.avatar:
            return None
        if request is None:
            return obj.avatar.url          # relative path
        return request.build_absolute_uri(obj.avatar.url)  # full URL

obj.avatar.url zwraca ścieżkę względną, np. /media/avatars/photo.jpg. request.build_absolute_uri(...) poprzedza ją schematem i hostem, tworząc http://127.0.0.1:8000/media/avatars/photo.jpg. Ten pełny adres URL jest bezpośrednio użyteczny we frontendzie bez konieczności łączenia ciągów znaków.

Przekazywanie żądania do serializer-a: get_serializer_context() jest nadpisywany na ViewSet, aby obiekt request był dostępny w context serializer-a:

def get_serializer_context(self):
    ctx = super().get_serializer_context()
    ctx["request"] = self.request
    return ctx

ModelViewSet wywołuje get_serializer_context() automatycznie podczas tworzenia instancji serializer-a — nie jest wymagane żadne dodatkowe okablowanie w metodach obsługi.

parser_classes = [MultiPartParser, FormParser] informuje DRF, że ma parsować treść żądania jako dane wieloczęściowego formularza (przy przesyłaniu plików) lub jako dane zakodowane w URL. Bez tego ustawienia DRF domyślnie parsuje JSON i przesyłanie plików kończyłoby się błędem 415 Unsupported Media Type.

Konfiguracja

pip install Django djangorestframework pillow
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

# Create a profile (multipart)
curl -X POST http://127.0.0.1:8000/api/profiles/ \
  -F "display_name=Alice" \
  -F "bio=Developer" \
  -F "avatar=@./photo.jpg"

# Retrieve — note avatar_url is a full http:// URL
curl http://127.0.0.1:8000/api/profiles/1/

# Update bio only (no file change)
curl -X PATCH http://127.0.0.1:8000/api/profiles/1/ \
  -F "bio=Senior Developer"

Zadanie 04: Menedżer zadań z zadaniami w tle

Lokalizacja: django-assignments/restapi/04-task-manager-bg-tasks/

Czego się nauczysz

  • Niestandardowa metoda update() na serializer-ze
  • Hook perform_update na ViewSet
  • Automatyczne ustawianie completed_at za pomocą timezone.now()
  • transaction.on_commit w kontekście API

Kluczowe koncepcje

Niestandardowa metoda update() na serializer-ze wykonuje dodatkową logikę po standardowej aktualizacji pól:

def update(self, instance, validated_data):
    completed_before = instance.completed
    instance = super().update(instance, validated_data)
    if not completed_before and instance.completed and instance.completed_at is None:
        instance.completed_at = timezone.now()
        instance.save(update_fields=["completed_at"])
    return instance

Sprawdzenie not completed_before and instance.completed zapewnia, że completed_at jest ustawiane tylko podczas przejścia ze stanu niezakończonego do zakończonego — nie przy aktualizacji zadania już oznaczonego jako zakończone i nie gdy klient jawnie ustawia completed: false.

perform_update na ViewSet jest wywoływany po serializer.save() i jest odpowiednim miejscem na efekty uboczne, które nie powinny znajdować się wewnątrz serializer-a:

def perform_update(self, serializer):
    before  = self.get_object()       # state before save
    updated = serializer.save()       # triggers serializer.update()
    if not before.completed and updated.completed:
        _notify_after_commit(updated.id)

Rozdzielenie odpowiedzialności: serializer obsługuje transformację danych; hook ViewSet obsługuje efekty uboczne na poziomie aplikacji.

timezone.now() zwraca obiekt datetime ze strefą czasową, korzystając ze strefy skonfigurowanej w settings.py (USE_TZ = True). W aplikacjach Django zawsze należy preferować to nad datetime.datetime.now().

Konfiguracja

pip install Django djangorestframework
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

# Create
curl -X POST http://127.0.0.1:8000/api/tasks/ \
  -H "Content-Type: application/json" \
  -d '{"title": "Write report"}'

# Complete (PATCH — partial update)
curl -X PATCH http://127.0.0.1:8000/api/tasks/1/ \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# Retrieve — note completed_at is now set
curl http://127.0.0.1:8000/api/tasks/1/

# Patch again — completed_at does not change
curl -X PATCH http://127.0.0.1:8000/api/tasks/1/ \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

Zadanie 05: Panel pogodowy

Lokalizacja: django-assignments/restapi/05-weather-dashboard/

Czego się nauczysz

  • request.query_params w widokach DRF
  • Zwracanie pola source informującego, czy dane pochodzą z cache, czy ze źródła na żywo
  • Obsługa błędów zewnętrznego API odpowiedzią 502 Bad Gateway
  • Widoki DRF oparte na funkcjach z dekoratorem @api_view

Kluczowe koncepcje

@api_view(["GET"]) jest odpowiednikiem APIView opartym na funkcjach. Nadaje się do jednorazowych endpoint-ów, które nie pasują do wzorca CRUD stosowanego w ViewSet. Udekorowana funkcja otrzymuje obiekt Request DRF i musi zwracać obiekt Response DRF.

request.query_params to nazwa DRF dla request.GET. Jest obiektem QueryDict:

city = (request.query_params.get("city") or "").strip()
if not city:
    return Response({"detail": "city is required"}, status=400)

Jawne pole source w odpowiedzi:

if cached is not None:
    return Response({"source": "cache", "city": city, "data": cached})
# ...
return Response({"source": "api",   "city": city, "data": data})

Umieszczenie pola source w odpowiedzi pozwala klientowi (i programiście) odróżnić wynik na żywo od wyniku z cache bez konieczności przeglądania logów serwera.

status=502 (Bad Gateway) to semantycznie właściwy kod statusu, gdy zewnętrzne API zawiedzie. Serwer odebrał żądanie i je zrozumiał, ale nie był w stanie uzyskać prawidłowej odpowiedzi od zewnętrznej usługi.

Konfiguracja

pip install Django djangorestframework requests
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

# Live API call
curl "http://127.0.0.1:8000/api/weather/?city=London"
# Response includes "source": "api"

# Second call within 5 minutes — from cache
curl "http://127.0.0.1:8000/api/weather/?city=London"
# Response includes "source": "cache"

# Missing city parameter
curl "http://127.0.0.1:8000/api/weather/"
# 400: {"detail": "city is required"}

Zadanie 06: Czat w czasie rzeczywistym

Lokalizacja: django-assignments/restapi/06-real-time-chat/

Czego się nauczysz

  • Łączenie REST API (DRF ViewSet) z WebSocket (Django Channels) w jednym projekcie
  • perform_create na ViewSet dla efektów ubocznych po utworzeniu zasobu
  • async_to_sync do wywoływania asynchronicznego kodu channel layer z synchronicznego widoku DRF
  • Jak transport REST i WebSocket uzupełniają się nawzajem

Kluczowe koncepcje

Dwa transporty, jeden model: Rekordy Message są przechowywane w bazie danych. MessageViewSet DRF obsługuje GET /api/messages/ (historia wiadomości) oraz POST /api/messages/ (tworzenie wiadomości). Konsument WebSocket obsługuje dostarczanie w czasie rzeczywistym. Wysłanie wiadomości przez REST API powoduje też jej push do wszystkich klientów podłączonych przez WebSocket:

class MessageViewSet(ModelViewSet):
    def perform_create(self, serializer):
        msg          = serializer.save()
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.group_send)(
            "chat",
            {"type": "chat.message", "username": msg.username, "text": msg.text},
        )

async_to_sync z biblioteki asgiref opakowuje funkcję asynchroniczną tak, by można ją było wywołać z kodu synchronicznego. Widoki DRF są synchroniczne; group_send Django Channels jest asynchroniczną coroutine. async_to_sync tworzy most między tymi dwoma światami.

Konsumenci WebSocket działają identycznie jak w wariancie klasycznym. ChatConsumer dołącza się do grupy "chat" przy połączeniu i przekazuje zdarzenia chat.message do klienta:

async def chat_message(self, event):
    await self.send(text_data=json.dumps({
        "username": event["username"],
        "text":     event["text"]
    }))

Ta metoda jest wywoływana przez channel layer za każdym razem, gdy group_send opublikuje zdarzenie {"type": "chat.message", ...} do grupy "chat".

Konfiguracja

pip install Django djangorestframework channels channels-redis
python manage.py migrate
daphne config.asgi:application   # or: uvicorn config.asgi:application

Testowanie krok po kroku

  1. Uruchom serwer za pomocą Daphne lub Uvicorn.
  2. Otwórz http://127.0.0.1:8000/ w dwóch kartach przeglądarki (szablon dostarcza interfejs czatu oparty na WebSocket).
  3. Wyślij wiadomość z karty 1. Pojawia się w karcie 2 natychmiastowo.
  4. W osobnym terminalu wyślij wiadomość przez REST API:
    curl -X POST http://127.0.0.1:8000/api/messages/ \
      -H "Content-Type: application/json" \
      -d '{"username": "api-client", "text": "Hello from REST"}'
    
    Wiadomość pojawia się w obu kartach przeglądarki — REST API i WebSocket są ze sobą połączone.

Zadanie 07: Obsługa formularzy i walidacja

Lokalizacja: django-assignments/restapi/07-forms-templates/

Czego się nauczysz

  • Używanie ViewSet jako endpoint-u wyłącznie do walidacji i zapisu
  • Ograniczanie dozwolonych metod HTTP na ViewSet za pomocą http_method_names
  • Zależność koncepcyjna między serializer-ami DRF a formularzami HTML

Kluczowe koncepcje

http_method_names = ["get", "post", "head", "options"] ogranicza ViewSet do obsługi wyłącznie GET i POST. Metody DELETE, PUT i PATCH zwracają odpowiedź 405 Method Not Allowed. Jest to odpowiednie podejście dla endpoint-u formularza kontaktowego — można wysyłać wiadomości i je pobierać, ale nie można ich edytować ani usuwać.

class ContactMessageViewSet(ModelViewSet):
    queryset         = ContactMessage.objects.all().order_by("-created_at")
    serializer_class = ContactMessageSerializer
    http_method_names = ["get", "post", "head", "options"]

Błędy walidacji DRF mają strukturę JSON. Jeśli brakuje pola email lub topic, DRF zwraca:

{
  "email": ["This field is required."],
  "topic": ["This field is required."]
}

Ten przewidywalny format pozwala frontendowi JavaScript wyświetlać komunikaty błędów per-pole obok odpowiedniego pola formularza.

EmailField w serializer-ze automatycznie waliduje format adresu e-mail. Nie jest potrzebny żaden niestandardowy walidator — DRF dziedziczy logikę walidacji z EmailField Django.

Konfiguracja

pip install Django djangorestframework
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

# Valid submission
curl -X POST http://127.0.0.1:8000/api/contact/ \
  -H "Content-Type: application/json" \
  -d '{"email": "alice@example.com", "topic": "Support", "message": "Hello"}'

# Missing fields — structured error response
curl -X POST http://127.0.0.1:8000/api/contact/ \
  -H "Content-Type: application/json" \
  -d '{"email": "not-an-email"}'

# List all messages
curl http://127.0.0.1:8000/api/contact/

# Attempt DELETE — 405
curl -X DELETE http://127.0.0.1:8000/api/contact/1/

Zadanie 08: Odbiornik webhooków

Lokalizacja: django-assignments/restapi/08-webhook-receiver/

Czego się nauczysz

  • Odczytywanie request.body w widoku DRF
  • HMAC + znacznik czasu + deduplikacja (ten sam model bezpieczeństwa co w wariancie klasycznym)
  • Dlaczego APIView DRF jest domyślnie zwolniony z ochrony CSRF
  • Tworzenie odpowiedzi przed walidacją podpisu i po niej

Kluczowe koncepcje

DRF i CSRF: Standardowe widoki Django wymagają tokena CSRF dla żądań POST z przeglądarek. APIView DRF (oraz @api_view) są zwolnione z CSRF dla uwierzytelniania bezsesyjnego, ponieważ są przeznaczone dla klientów API, którzy nie utrzymują sesji. Dekorator @csrf_exempt nie jest potrzebny.

request.body w DRF: Atrybut request.body działa identycznie jak w standardowym Django. Musi być odczytany przed dostępem do request.data (który parsuje treść, opróżniając strumień). Wzorzec:

raw      = request.body                        # raw bytes
provided = request.headers.get("X-Signature", "")
expected = hmac.new(
    settings.WEBHOOK_SECRET.encode("utf-8"),
    raw,
    hashlib.sha256
).hexdigest()
valid = hmac.compare_digest(expected, provided)

try:
    payload = json.loads(raw.decode("utf-8"))
except Exception:
    payload = {"raw": raw.decode("utf-8", errors="replace")}

Należy zwrócić uwagę, że zdarzenie jest zawsze zapisywane, nawet jeśli podpis jest nieprawidłowy (signature_valid=False). Ten dziennik audytu jest cenny: można przejrzeć odrzucone zdarzenia w celu wykrycia ataków lub nieprawidłowo skonfigurowanych nadawców.

Trasy URL z parametrem dostawcy (/api/webhooks/{provider}/) umożliwiają jednej aplikacji odbieranie webhooków od wielu usług (GitHub, Stripe itp.) i kierowanie ich na podstawie parametru provider.

Konfiguracja

pip install Django djangorestframework
python manage.py migrate
python manage.py runserver

Przed testowaniem ustaw WEBHOOK_SECRET = "change-me" w config/settings.py.

Testowanie krok po kroku

import hashlib, hmac, json, time
import requests

SECRET   = "change-me"
body     = json.dumps({"event": "push"}).encode()
ts       = str(int(time.time()))
event_id = "evt-abc-001"
sig      = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()

resp = requests.post(
    "http://127.0.0.1:8000/api/webhooks/github/",
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-Signature":  sig,
        "X-Timestamp":  ts,
        "X-Event-Id":   event_id,
    }
)
print(resp.status_code, resp.json())
  • Pierwsze uruchomienie → 200 {"status": "ok"}
  • Drugie uruchomienie z tym samym event_id409 {"detail": "duplicate event"}
  • Błędny sekret → 401 {"detail": "invalid signature"}

Zadanie 09: Zarządzanie magazynem

Lokalizacja: django-assignments/restapi/09-inventory-management/

Czego się nauczysz

  • Dwa osobne router-y DRF do wersjonowania API
  • SerializerMethodField dla obliczanego pola logicznego
  • Filtrowanie za pomocą obiektów Q wewnątrz get_queryset
  • Różnica w kształcie odpowiedzi między V1 a V2

Kluczowe koncepcje

Dwa router-y, dwa ViewSet-y:

router_v1 = DefaultRouter()
router_v1.register("items", ItemV1ViewSet, basename="item-v1")

router_v2 = DefaultRouter()
router_v2.register("items", ItemV2ViewSet, basename="item-v2")

urlpatterns = [
    path("api/v1/", include(router_v1.urls)),
    path("api/v2/", include(router_v2.urls)),
]

ItemV1ViewSet używa ItemV1Serializer (bez obliczanych pól). ItemV2ViewSet używa ItemV2Serializer, który dodaje pole in_stock:

class ItemV2Serializer(ModelSerializer):
    in_stock = SerializerMethodField()
    ...
    def get_in_stock(self, obj):
        return obj.quantity > 0

in_stock to pole pochodne — nie jest przechowywane w bazie danych, lecz obliczane na podstawie quantity przy każdej serializacji. Jest to elegancki sposób dodawania właściwości obliczanych według logiki biznesowej do odpowiedzi API bez modyfikowania modelu danych.

get_queryset do filtrowania wyszukiwania: Oba ViewSet-y nadpisują get_queryset, aby zastosować opcjonalne wyszukiwanie:

def get_queryset(self):
    qs = Item.objects.all().order_by("-created_at")
    q  = (self.request.query_params.get("q") or "").strip()
    if q:
        qs = qs.filter(Q(name__icontains=q) | Q(sku__icontains=q))
    return qs

Nadpisanie get_queryset to właściwe miejsce do filtrowania w ViewSet, ponieważ stosuje się spójnie do akcji list, retrieve i wszystkich innych.

Paginacja DRF: Dodaj do config/settings.py, aby włączyć automatyczną paginację:

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
    "PAGE_SIZE": 20,
}

Przy tym ustawieniu GET /api/v1/items/ zwraca {"count": N, "next": "...", "previous": "...", "results": [...]}.

Konfiguracja

pip install Django djangorestframework
python manage.py makemigrations
python manage.py migrate
python manage.py runserver

Testowanie krok po kroku

# V1 — no in_stock field
curl "http://127.0.0.1:8000/api/v1/items/"

# V2 — includes in_stock
curl "http://127.0.0.1:8000/api/v2/items/"

# Search
curl "http://127.0.0.1:8000/api/v2/items/?q=bolt"

# Create an item, then check in_stock
curl -X POST http://127.0.0.1:8000/api/v1/items/ \
  -H "Content-Type: application/json" \
  -d '{"name": "Steel bolt", "sku": "BOLT-001", "quantity": 0}'

curl http://127.0.0.1:8000/api/v2/items/1/
# "in_stock": false  (quantity is 0)

Zadanie 10: Bezpieczny sejf dokumentów

Lokalizacja: django-assignments/restapi/10-secure-document-vault/

Czego się nauczysz

  • Uwierzytelnianie JWT z biblioteką djangorestframework-simplejwt
  • permission_classes = [IsAuthenticated] na ViewSet
  • perform_create do automatycznego przypisywania uwierzytelnionego użytkownika jako właściciela
  • get_queryset do filtrowania na poziomie obiektów na podstawie bieżącego użytkownika
  • SerializerMethodField dla pełnego adresu URL pliku

Kluczowe koncepcje

djangorestframework-simplejwt dostarcza dwa endpoint-y gotowe do użycia, podłączone bezpośrednio w urls.py:

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [
    path("api/token/",         TokenObtainPairView.as_view()),
    path("api/token/refresh/", TokenRefreshView.as_view()),
    path("api/", include(router.urls)),
]

POST /api/token/ z payload-em {"username": "...", "password": "..."} zwraca {"access": "...", "refresh": "..."}. Token dostępu należy dołączyć do kolejnych żądań w header-ze: Authorization: Bearer <access>.

permission_classes = [IsAuthenticated] odrzuca żądania bez prawidłowego tokena odpowiedzią 401 Unauthorized. Cały ViewSet jest chroniony — żaden endpoint nie jest publicznie dostępny.

perform_create jest wywoływany przez ViewSet po serializer.is_valid(), ale przed zwróceniem odpowiedzi. To właściwe miejsce, aby wstrzyknąć właściciela:

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

Pole owner jest oznaczone jako read_only w serializer-ze, aby klienci nie mogli sfałszować właścicielstwa. ViewSet ustawia je na podstawie uwierzytelnionego użytkownika.

get_queryset do izolacji na poziomie obiektów:

def get_queryset(self):
    user = self.request.user
    if _is_admin(user):
        return Document.objects.all().order_by("-created_at")
    return Document.objects.filter(owner=user).order_by("-created_at")

Użytkownik niebędący administratorem może widzieć i pobierać wyłącznie własne dokumenty. Próba wykonania GET /api/documents/5/ dla dokumentu należącego do innego użytkownika zwraca 404 Not Found, a nie 403 Forbidden. Odpowiedź 404 nie ujawnia, czy dokument o identyfikatorze 5 w ogóle istnieje.

file_url z SerializerMethodField generuje bezwzględny adres URL identycznie jak w zadaniu 03:

def get_file_url(self, obj):
    request = self.context.get("request")
    if not obj.file:
        return None
    return request.build_absolute_uri(obj.file.url) if request else obj.file.url

Konfiguracja

pip install Django djangorestframework djangorestframework-simplejwt
python manage.py migrate
python manage.py createsuperuser   # creates the first user for testing
python manage.py runserver

Testowanie krok po kroku

# Get a JWT token for the superuser
curl -X POST http://127.0.0.1:8000/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "your_password"}'

# Store the access token
TOKEN="<access_token_from_response>"

# Upload a document
curl -X POST http://127.0.0.1:8000/api/documents/ \
  -H "Authorization: Bearer $TOKEN" \
  -F "title=My Report" \
  -F "file=@./report.pdf"

# List documents
curl http://127.0.0.1:8000/api/documents/ \
  -H "Authorization: Bearer $TOKEN"

# No token — 401
curl http://127.0.0.1:8000/api/documents/

Test wieloużytkownikowy: Utwórz drugiego użytkownika w panelu administracyjnym Django. Pobierz token dla tego użytkownika. Wylistuj dokumenty — lista jest pusta (drugi użytkownik nie jest właścicielem żadnych dokumentów). Spróbuj pobrać GET /api/documents/1/ — zostanie zwrócony kod 404, nie 403.

Odświeżanie tokena: Tokeny dostępu wygasają (domyślnie po 5 minutach w simplejwt). Użyj tokena odświeżającego:

curl -X POST http://127.0.0.1:8000/api/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{"refresh": "<refresh_token>"}'