Table of Contents
- Django REST Framework — Kompletny przewodnik po zadaniach
- Wymagania wstępne
- Styl kodu DRF stosowany w zadaniach
- Zadanie 01: Podstawowe API listy zadań
- Zadanie 02: Blog osobisty
- Zadanie 03: Profile użytkowników i przesyłanie plików
- Zadanie 04: Menedżer zadań z zadaniami w tle
- Zadanie 05: Panel pogodowy
- Zadanie 06: Czat w czasie rzeczywistym
- Zadanie 07: Obsługa formularzy i walidacja
- Zadanie 08: Odbiornik webhooków
- Zadanie 09: Zarządzanie magazynem
- Zadanie 10: Bezpieczny sejf dokumentów
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
ModelSerializeriread_only_fieldsModelViewSetdla pełnego CRUD w minimalnej liczbie linii koduDefaultRouterdo automatycznego generowania tras URL- Pole
updated_atorazauto_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_fieldna ViewSet do wyszukiwania po innym polu niż klucz główny- Walidacja
SlugFieldw 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
MultiPartParseriFormParserna ViewSetSerializerMethodFielddla obliczanych pól w odpowiedzirequest.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_updatena ViewSet - Automatyczne ustawianie
completed_atza pomocątimezone.now() transaction.on_commitw 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_paramsw widokach DRF- Zwracanie pola
sourceinformują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_createna ViewSet dla efektów ubocznych po utworzeniu zasobuasync_to_syncdo 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
- Uruchom serwer za pomocą Daphne lub Uvicorn.
- Otwórz
http://127.0.0.1:8000/w dwóch kartach przeglądarki (szablon dostarcza interfejs czatu oparty na WebSocket). - Wyślij wiadomość z karty 1. Pojawia się w karcie 2 natychmiastowo.
- W osobnym terminalu wyślij wiadomość przez REST API:
Wiadomość pojawia się w obu kartach przeglądarki — REST API i WebSocket są ze sobą połączone.curl -X POST http://127.0.0.1:8000/api/messages/ \ -H "Content-Type: application/json" \ -d '{"username": "api-client", "text": "Hello from REST"}'
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.bodyw widoku DRF - HMAC + znacznik czasu + deduplikacja (ten sam model bezpieczeństwa co w wariancie klasycznym)
- Dlaczego
APIViewDRF 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_id→409 {"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
SerializerMethodFielddla obliczanego pola logicznego- Filtrowanie za pomocą obiektów
Qwewnątrzget_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 ViewSetperform_createdo automatycznego przypisywania uwierzytelnionego użytkownika jako właścicielaget_querysetdo filtrowania na poziomie obiektów na podstawie bieżącego użytkownikaSerializerMethodFielddla 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>"}'