Ana içeriğe geç
Kesit Python
İleri · Web ve API

Django'ya giriş: MVT, ORM, admin ve DRF

Django projesi ve uygulaması kurar; URL, view, model, migration, admin, şablon, form ve kimlik doğrulamayı tanır; ORM sorgularını (filter, exclude, annotate, aggregate, select_related) SQL karşılıklarıyla birlikte düşünmeyi Anadolu Market örneğiyle öğrenirsin.

Uygulamada aç, 30 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden Django?

Flask ve FastAPI derslerinde küçük, esnek çatıları (framework) gördün: yönlendirme verirler, gerisini sen seçersin. Django ise "pili içinde" (batteries included) yaklaşımıyla gelir: ORM, migration sistemi, admin paneli, kullanıcı ve yetki sistemi, form doğrulama, şablon motoru, önbellek (cache), e-posta ve güvenlik korumaları tek pakette ve birbiriyle uyumlu. Anadolu Market'in iç yönetim paneli, Ilgaz Bank'ın kredi başvuru takip ekranı ya da belediyenin randevu sistemi gibi veritabanı ağırlıklı, formlu, yetkili kullanıcılı uygulamalarda Django ile birkaç günde çalışan bir ürün çıkarabilirsin.

Django tarayıcıda çalışmazBu dersteki Django kodları pre kutularında gösterim içindir; kendi bilgisayarında denemen gerekir. Çalıştırılabilir kutular ve alıştırmalar ise Django'nun arkasındaki mantığı (QuerySet, URL eşleme, form doğrulama, migration SQL'i) saf Python ve sqlite3 ile taklit eder. Böylece Django'yu "sihir" olarak değil, anlaşılır bir makine olarak öğrenirsin.

Kurulum: proje ve uygulama

  1. Proje klasörü aç ve sanal ortam (virtual environment) kur (Sanal ortam ve pip dersini hatırla).
  2. Django'yu kur, projeyi oluştur, bir uygulama (app) ekle.
  3. Veritabanı tablolarını oluştur ve geliştirme sunucusunu başlat; tarayıcıda http://127.0.0.1:8000/ adresini aç.
# Windows (PowerShell)
python -m venv .venv
.venv\Scripts\Activate.ps1
# macOS / Linux
python3 -m venv .venv
source .venv/bin/activate

pip install django
django-admin startproject anadolu_market .
python manage.py startapp magaza
python manage.py migrate
python manage.py runserver

Komutların sonunda şu yapı oluşur:

anadolu_market/        # proje paketi: ayarlar ve kök URL'ler
    settings.py
    urls.py
    wsgi.py / asgi.py  # sunucunun giriş noktaları
magaza/                # uygulama: tek bir iş alanı
    models.py  views.py  admin.py  apps.py  tests.py
    migrations/
manage.py              # yönetim komutlarının kapısı
KavramNe?Anadolu Market örneği
Proje (project)Ayarlar, kök URL'ler, uygulamaların toplamıanadolu_market
Uygulama (app)Tek bir iş alanının model, view ve şablonları; başka projeye taşınabilirmagaza, siparis, musteri
INSTALLED_APPSProjenin tanıdığı uygulamalar"magaza.apps.MagazaConfig" eklenmezse modeller görünmez

Yeni uygulamayı settings.py içindeki INSTALLED_APPS listesine eklemeyi unutma. Aynı dosyada DATABASES (varsayılan SQLite), DEBUG, ALLOWED_HOSTS, SECRET_KEY, TEMPLATES ve STATIC_URL ayarları bulunur.

MVT: Model, View, Template

Django kendi mimarisine MVT der. Klasik MVC ile eşleşmesi biraz kafa karıştırır: Django'nun "view"u MVC'deki controller'a, "template"i MVC'deki view'a karşılık gelir.

KatmanGöreviDosya
ModelVeri yapısı ve iş kuralları; tabloya eşlenirmodels.py
Viewİsteği alır, modelden veri çeker, yanıt döndürürviews.py
TemplateHTML üretimi; mantıktan arındırılmış sunumtemplates/
URLconfYolu doğru view'a yönlendirirurls.py

Bir isteğin yolculuğu: tarayıcı /urunler/42/ ister → ara katmanlar (middleware: oturum, CSRF, güvenlik başlıkları) çalışır → URLconf deseni eşler ve pk=42 yakalar → view modeli sorgular → şablon HTML üretir → yanıt yine ara katmanlardan geçip döner.

URL'ler ve view'lar

# magaza/urls.py
from django.urls import path
from . import views

app_name = "magaza"
urlpatterns = [
    path("", views.urun_listesi, name="liste"),
    path("urunler/yeni/", views.UrunOlustur.as_view(), name="yeni"),
    path("urunler/<int:pk>/", views.UrunDetay.as_view(), name="detay"),
    path("kategori/<slug:slug>/", views.kategori, name="kategori"),
]
# anadolu_market/urls.py içinde: path("magaza/", include("magaza.urls"))

Desenler yukarıdan aşağıya denenir ve ilk eşleşen kazanır. <int:pk> bir dönüştürücüdür (converter): yalnız rakamları eşler ve değeri int olarak view'a verir. Diğerleri: str (eğik çizgi hariç her şey), slug, uuid, path. Arka planda her desen bir düzenli ifadeye çevrilir; aşağıdaki kutu bu fikri taklit ediyor.

import re

DONUSTURUCU = {"int": (r"[0-9]+", int), "str": (r"[^/]+", str), "slug": (r"[-a-zA-Z0-9_]+", str)}

def derle(desen):
    cevirici = {}
    def degistir(m):
        tur, ad = m.group(1), m.group(2)
        cevirici[ad] = DONUSTURUCU[tur][1]
        return f"(?P<{ad}>{DONUSTURUCU[tur][0]})"
    regex = re.sub(r"<(\w+):(\w+)>", degistir, desen)
    return re.compile("^" + regex + "$"), cevirici

regex, cevirici = derle("urunler/<int:pk>/")
for yol in ["urunler/42/", "urunler/abc/", "urunler/7"]:
    m = regex.match(yol)
    if m:
        print(yol, "->", {k: cevirici[k](v) for k, v in m.groupdict().items()})
    else:
        print(yol, "-> 404")

View iki biçimde yazılır. Fonksiyon tabanlı view (FBV) açık ve basittir; sınıf tabanlı view (CBV) listeleme, detay, oluşturma gibi tekrarlayan kalıpları hazır verir.

# magaza/views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import DetailView, CreateView
from django.contrib.auth.decorators import login_required
from .models import Urun

def urun_listesi(request):
    urunler = Urun.objects.filter(aktif=True).order_by("-eklenme")
    return render(request, "magaza/liste.html", {"urunler": urunler})

class UrunDetay(DetailView):
    model = Urun                       # pk'yı URL'den alır, yoksa 404
    template_name = "magaza/detay.html"

class UrunOlustur(CreateView):
    model = Urun
    fields = ["ad", "kategori", "fiyat", "stok"]
    success_url = "/magaza/"

Modeller ve ORM

Model, bir tabloyu temsil eden Python sınıfıdır. Her alan (field) bir sütundur; id birincil anahtarı otomatik eklenir.

# magaza/models.py
from django.db import models

class Kategori(models.Model):
    ad = models.CharField(max_length=60, unique=True)

class Urun(models.Model):
    ad = models.CharField(max_length=120)
    kategori = models.ForeignKey(Kategori, on_delete=models.PROTECT, related_name="urunler")
    fiyat = models.DecimalField(max_digits=10, decimal_places=2)
    stok = models.PositiveIntegerField(default=0)
    aktif = models.BooleanField(default=True)
    eklenme = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.ad

Urun.objects bir yöneticidir (manager); ondan dönen QuerySet iki özelliğe sahiptir: tembeldir (lazy), yani üzerinde döngü kurana, len, list ya da dilim uygulayana kadar veritabanına gitmez; ve zincirlenebilirdir, her filter yeni bir QuerySet döndürür, eskisini değiştirmez. Alan adı ile işlem arasına çift alt çizgi koyarak sorgu yaparsın; buna lookup denir.

ORMSQL karşılığı (yaklaşık)
filter(fiyat__gt=100)WHERE fiyat > 100
filter(ad__icontains="çay")WHERE ad LIKE '%çay%' (büyük/küçük harf duyarsız)
filter(sehir__in=["İzmir", "Bursa"])WHERE sehir IN (...)
exclude(stok=0)WHERE NOT (stok = 0)
filter(kategori__ad="İçecek")JOIN kategori ... WHERE kategori.ad = ...
aggregate(Sum("fiyat"))SELECT SUM(fiyat) → tek sözlük
values("kategori").annotate(n=Count("id"))GROUP BY kategori → satır başına değer
order_by("-fiyat")[:5]ORDER BY fiyat DESC LIMIT 5
from django.db.models import Q, F, Count, Sum, Avg

pahali = Urun.objects.filter(fiyat__gte=500).exclude(stok=0)
print(pahali.query)            # üretilen SQL'i görürsün
izmir_ya_da_buyuk = Siparis.objects.filter(Q(sehir="İzmir") | Q(tutar__gt=1000))
Urun.objects.filter(stok__lt=F("kritik_stok"))          # iki sütunu karşılaştır
Urun.objects.aggregate(toplam=Sum("fiyat"), ort=Avg("fiyat"))
Kategori.objects.annotate(urun_sayisi=Count("urunler")).order_by("-urun_sayisi")
Urun.objects.select_related("kategori")                 # tek JOIN, N+1 yok
Kategori.objects.prefetch_related("urunler")            # 2 sorgu, ters ilişki

Aşağıdaki kutu, zincirlenebilir ve lookup destekli bir QuerySet'in özünü 20 satırda taklit ediyor. Alıştırmalarda bunu adım adım sen kuracaksın.

ISLEM = {
    "exact": lambda a, b: a == b,
    "gt": lambda a, b: a > b,
    "lte": lambda a, b: a <= b,
    "icontains": lambda a, b: b.lower() in a.lower(),
}

class Sorgu:
    def __init__(self, kayitlar, kosullar=()):
        self.kayitlar, self.kosullar = kayitlar, list(kosullar)
    def filter(self, **kw):
        return Sorgu(self.kayitlar, self.kosullar + list(kw.items()))
    def __iter__(self):                      # tembel: ancak gezilince çalışır
        for k in self.kayitlar:
            if all(ISLEM[(a.split("__") + ["exact"])[1]](k[a.split("__")[0]], v)
                   for a, v in self.kosullar):
                yield k

urunler = [{"ad": "Rize Çayı", "fiyat": 120}, {"ad": "Türk Kahvesi", "fiyat": 350},
           {"ad": "Yeşil Çay", "fiyat": 90}]
qs = Sorgu(urunler).filter(ad__icontains="çay")
ucuz = qs.filter(fiyat__lte=100)
print([u["ad"] for u in qs])
print([u["ad"] for u in ucuz])

ORM sonunda SQL üretir. Aynı sorgunun SQL karşılığını sqlite3 ile görmek, Django'da hangi sorgunun pahalı olduğunu sezmeni sağlar:

import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE kategori (id INTEGER PRIMARY KEY, ad TEXT);
CREATE TABLE urun (id INTEGER PRIMARY KEY, ad TEXT, fiyat REAL, kategori_id INTEGER);
INSERT INTO kategori VALUES (1, 'İçecek'), (2, 'Mutfak');
INSERT INTO urun VALUES (1, 'Çay', 120, 1), (2, 'Kahve', 350, 1), (3, 'Bardak', 25, 2);
""")
# Kategori.objects.annotate(n=Count("urunler"), ort=Avg("urunler__fiyat")).order_by("-n")
sql = """SELECT k.ad, COUNT(u.id) AS n, AVG(u.fiyat) AS ort
         FROM kategori k LEFT JOIN urun u ON u.kategori_id = k.id
         GROUP BY k.id ORDER BY n DESC"""
for satir in db.execute(sql):
    print(satir)

Migration: modelden tabloya

Modeli değiştirdiğinde veritabanı kendiliğinden değişmez. İki adım vardır: makemigrations modeldeki farkı bulup migrations/0002_...py dosyası yazar; migrate bu dosyaları veritabanına uygular. Migration dosyaları git'e girer; ekipteki herkes ve sunucu aynı şemaya böyle ulaşır.

python manage.py makemigrations magaza
python manage.py sqlmigrate magaza 0001     # üretilecek SQL'i göster
python manage.py showmigrations
python manage.py migrate

Django'nun yaptığını küçük ölçekte taklit edelim: dataclass alanlarından CREATE TABLE üretmek.

from dataclasses import dataclass, fields

TIP = {int: "INTEGER", str: "VARCHAR(120)", float: "REAL", bool: "BOOL"}

@dataclass
class Musteri:
    ad: str
    sehir: str
    puan: float
    aktif: bool

def create_table(model):
    sutunlar = ['"id" INTEGER PRIMARY KEY AUTOINCREMENT']
    sutunlar += [f'"{f.name}" {TIP[f.type]} NOT NULL' for f in fields(model)]
    return f'CREATE TABLE "magaza_{model.__name__.lower()}" (' + ", ".join(sutunlar) + ");"

print(create_table(Musteri))

Tablo adının magaza_musteri olmasına dikkat: Django varsayılan olarak uygulama_model adını kullanır.

Admin paneli

Django'nun en çok sevilen özelliği: birkaç satırla, yetkili personelin veriyi listeleyip arayıp düzenleyebileceği bir yönetim paneli.

# magaza/admin.py
from django.contrib import admin
from .models import Urun

@admin.register(Urun)
class UrunAdmin(admin.ModelAdmin):
    list_display = ["ad", "kategori", "fiyat", "stok", "aktif"]
    list_filter = ["kategori", "aktif"]
    search_fields = ["ad"]
    list_select_related = ["kategori"]

# terminalde: python manage.py createsuperuser  → /admin/ adresine gir

Şablonlar, formlar ve kimlik doğrulama

<!-- magaza/templates/magaza/liste.html -->
{% extends "base.html" %}
{% block icerik %}
  {% for u in urunler %}
    <p>{{ u.ad }} — {{ u.fiyat|floatformat:2 }} TL</p>
  {% empty %}
    <p>Ürün yok.</p>
  {% endfor %}
{% endblock %}

{{ u.ad }} çıktısı otomatik kaçırılır (autoescape): ürün adına <script> yazılsa bile metin olarak görünür. Formlarda ise ModelForm, model alanlarından form üretir; is_valid() her alanı dönüştürür, clean_<alan> metotlarındaki kuralları çalıştırır, hataları form.errors'a, temiz veriyi form.cleaned_data'ya koyar.

from django import forms

class UrunFormu(forms.ModelForm):
    class Meta:
        model = Urun
        fields = ["ad", "kategori", "fiyat", "stok"]

    def clean_fiyat(self):
        fiyat = self.cleaned_data["fiyat"]
        if fiyat <= 0:
            raise forms.ValidationError("Fiyat pozitif olmalı.")
        return fiyat

@login_required
def urun_ekle(request):
    form = UrunFormu(request.POST or None)
    if request.method == "POST" and form.is_valid():
        form.save()
        return redirect("magaza:liste")
    return render(request, "magaza/form.html", {"form": form})
# şablonda: <form method="post">{% csrf_token %}{{ form.as_p }}</form>

Aynı doğrulama akışının saf Python taklidi:

class UrunFormu:
    def __init__(self, veri):
        self.veri, self.errors, self.cleaned_data = veri, {}, {}
    def is_valid(self):
        ad = self.veri.get("ad", "").strip()
        if not ad:
            self.errors["ad"] = "Bu alan zorunlu."
        try:
            fiyat = float(self.veri.get("fiyat", ""))
            if fiyat <= 0:
                self.errors["fiyat"] = "Fiyat pozitif olmalı."
        except ValueError:
            self.errors["fiyat"] = "Geçerli bir sayı gir."
        if not self.errors:
            self.cleaned_data = {"ad": ad, "fiyat": fiyat}
        return not self.errors

f = UrunFormu({"ad": "  Rize Çayı ", "fiyat": "120.5"})
print(f.is_valid(), f.cleaned_data)
g = UrunFormu({"ad": "", "fiyat": "bedava"})
print(g.is_valid(), g.errors)

Kimlik doğrulama hazırdır: django.contrib.auth kullanıcı modelini, parola özetlemeyi (PBKDF2), giriş/çıkış view'larını, gruplar ve izinleri (user.has_perm("magaza.change_urun")) sağlar. View'ı korumak için @login_required ya da CBV'de LoginRequiredMixin kullanırsın.

Django REST Framework (DRF)

Mobil uygulama ya da React ön yüz için JSON API gerekiyorsa DRF eklenir (pip install djangorestframework). Serializer, ModelForm'un API karşılığıdır.

from rest_framework import serializers, viewsets, routers, permissions

class UrunSerializer(serializers.ModelSerializer):
    class Meta:
        model = Urun
        fields = ["id", "ad", "fiyat", "stok"]

class UrunViewSet(viewsets.ModelViewSet):
    queryset = Urun.objects.select_related("kategori").filter(aktif=True)
    serializer_class = UrunSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

router = routers.DefaultRouter()
router.register("urunler", UrunViewSet)   # GET/POST /urunler/, GET/PUT/DELETE /urunler/5/

Güvenlik varsayılanları ve dağıtım

TehditDjango'nun varsayılanıSeni bozan şey
SQL enjeksiyonuORM her değeri parametre olarak gönderirraw()/extra() içinde f-string
XSSŞablonlarda otomatik kaçış|safe, mark_safe ile kullanıcı verisi
CSRFCsrfViewMiddleware + {% csrf_token %}Gelişigüzel @csrf_exempt
ClickjackingX-Frame-Options: DENYMiddleware'i kaldırmak
ParolaPBKDF2 ile tuzlu özetParolayı kendi alanında düz saklamak

Yayına almadan önce: DEBUG = False, SECRET_KEY ortam değişkeninden, ALLOWED_HOSTS dolu, veritabanı PostgreSQL, statik dosyalar collectstatic ile toplanmış. Uygulamayı runserver ile değil, gunicorn (WSGI) ya da uvicorn (ASGI) ile çalıştırırsın; önüne nginx ya da bir platform (Render, Railway, Fly.io) konur. Docker ve dağıtım dersinde bu adımları konteynerle birleştireceksin.

python manage.py check --deploy        # güvenlik ayarlarını denetler
python manage.py collectstatic --noinput
gunicorn anadolu_market.wsgi:application --bind 0.0.0.0:8000 --workers 3
İlk projede sıralamaModel → makemigrations/migrate → admin'e kaydet → admin'den örnek veri gir → view + URL → şablon. Admin, view yazmadan verini görmeni sağlar; hata ayıklamayı (debugging) çok kısaltır.
Gerçek işteKurumsal Django projelerinde en sık performans sorunu N+1 sorgudur: 50 siparişlik bir listede her satır için müşteri adını şablonda {{ s.musteri.ad }} diye yazmak 1 + 50 sorgu üretir. Kod incelemesinde (code review) liste view'larında select_related/prefetch_related aranır; geliştirme sırasında django-debug-toolbar ile sayfa başına sorgu sayısı izlenir. İkinci klasik: modeli değiştirip migration dosyasını commit etmeyi unutmak; sunucuda "no such column" hatası alınır.
Derinleş

Derinleş: Django'da HTMX, form, fetch ile JSON ve SPA kararı

Django'nun şablon sistemi, formları ve admin paneli sunucu tarafı oluşturma (SSR) için kurulmuştur. Bir noktada şu karar gelir: arayüzü ayrı bir SPA (React) ve DRF API'siyle mi kuracağız, yoksa Django şablonlarını koruyup yalnız gereken yerleri dinamik mi yapacağız? İkinci yolun en yaygın aracı HTMX'tir. Flask dersindeki ek bölümde HTMX'in temel özniteliklerini (hx-get, hx-target, hx-swap, hx-trigger) gördüysen, burada aynı fikrin Django'daki karşılığını, CSRF'yi ve JSON uç noktalarını öğreneceksin.

SoruDjango şablonu + HTMXDRF + SPA
Arama motoru (SEO) önemli mi?Hazır HTML, doğalEk sunucu tarafı oluşturma gerekir
Mobil uygulama da aynı veriyi kullanacak mı?Ayrı JSON uç noktaları gerekirAynı API
EkipPython ağırlıklıAyrı ön yüz uzmanlığı
Admin, form, yetkiDjango'nun hazır araçları aynenAPI ve istemcide yeniden kurulur

Parça şablonu ve HX-Request

Django'da parça şablonu ayrı bir dosyadır ve tam sayfa şablonunda {% include %} ile kullanılır. Görünüm, request.headers ile HTMX isteğini tanır; django-htmx paketi bunu request.htmx olarak kısaltır. Sayfalamada Django'nun Paginator sınıfı kullanılır.

# urunler/views.py
from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Urun

def urun_listesi(request):
    q = request.GET.get("q", "")
    qs = Urun.objects.filter(ad__icontains=q).order_by("id")
    sayfa = Paginator(qs, 12).get_page(request.GET.get("sayfa"))
    baglam = {"sayfa": sayfa, "q": q}
    if request.headers.get("HX-Request") == "true":          # ya da request.htmx
        return render(request, "urunler/_satirlar.html", baglam)
    return render(request, "urunler/liste.html", baglam)
{# urunler/_satirlar.html #}
{% for u in sayfa %}
  <tr><td>{{ u.ad }}</td><td>{{ u.fiyat }} TL</td></tr>
{% endfor %}
{% if sayfa.has_next %}
  <tr hx-get="?q={{ q|urlencode }}&sayfa={{ sayfa.next_page_number }}"
      hx-trigger="revealed" hx-swap="outerHTML"><td>Yükleniyor...</td></tr>
{% endif %}

CSRF: Django'nun kapısı

Django, POST/PUT/PATCH/DELETE isteklerinde CSRF belirteci (token) ister. Klasik formda {% csrf_token %} gizli bir csrfmiddlewaretoken alanı ekler. HTMX ve fetch isteklerinde belirteç başlıkla gönderilir: X-CSRFToken. Belirtecin değeri csrftoken çerezinde de durur.

<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>   {# tüm HTMX istekleri başlığı taşır #}

<script>
function cerez(ad) {
  return document.cookie.split("; ").find(c => c.startsWith(ad + "="))?.split("=")[1];
}
const r = await fetch("/api/sepet/", {
  method: "POST",
  headers: {"Content-Type": "application/json", "X-CSRFToken": cerez("csrftoken")},
  body: JSON.stringify({urun_id: 19, adet: 2}),
});
</script>

Çerezden belirteç okumak tarayıcıda çalışan kodun işidir; mantığı Python'la görelim. Gerçek Anadolu Market ürünlerini veri modülünden okuyup JSON'a çeviriyoruz:

import json
from decimal import Decimal
from http.cookies import SimpleCookie
import veri

c = SimpleCookie()
c.load("sessionid=abc123; csrftoken=Xy9QeT2k")
print("çerezdeki belirteç:", c["csrftoken"].value)

urunler = veri.kayitlar("anadolu_market", "products")
kitaplar = [{"id": u["product_id"], "ad": u["name"], "fiyat": Decimal(str(u["price"]))}
            for u in urunler if u["category"] == "Kitap"][:3]

def ondalik(o):                      # DjangoJSONEncoder'ın yaptığı gibi
    if isinstance(o, Decimal):
        return str(o)
    raise TypeError(f"{type(o).__name__} JSON'a çevrilemez")

print(json.dumps(kitaplar, default=ondalik, ensure_ascii=False))

JsonResponse ve fetch

JSON uç noktası için DRF şart değildir; küçük işlerde JsonResponse yeter. İki ayrıntı: varsayılan safe=True yalnız sözlük kabul eder (liste için safe=False), ve DecimalField değerleri Django'nun kodlayıcısında metne çevrilir. Türkçe karakterlerin kaçırılmaması için json_dumps_params={"ensure_ascii": False} verilebilir.

import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST

@require_POST
def sepet_api(request):
    try:
        govde = json.loads(request.body)
    except json.JSONDecodeError:
        return JsonResponse({"hata": "geçersiz JSON"}, status=400)
    form = SepetFormu(govde)                       # Django formu JSON ile de çalışır
    if not form.is_valid():
        return JsonResponse({"hatalar": form.errors}, status=422)
    return JsonResponse({"tutar": str(form.tutar())})

def kategoriler(request):
    return JsonResponse(list(Kategori.objects.values("id", "ad")), safe=False,
                        json_dumps_params={"ensure_ascii": False})

Form hataları HTMX ile

HTMX 2, varsayılan olarak 4xx/5xx yanıtları sayfaya yerleştirmez. Form hatalarını göstermek için iki yol var: hatalı formu 200 ile parça olarak döndürmek, ya da 422 dönüp HX-Retarget ve HX-Reswap yanıt başlıklarıyla hataların nereye konacağını söylemek (ve istemcide 422'yi yerleştirmeye izin veren yapılandırmayı açmak). Hangi yolu seçersen seç, kural aynıdır: doğrulama her zaman sunucuda yapılır.

Gerçek işteDjango + HTMX, özellikle iç sistemlerde (bayi portalı, İK izin ekranı, stok paneli) yaygınlaşıyor: admin paneli, kimlik doğrulama ve form doğrulaması hazır gelir; ön yüz projesi gerekmez. Mobil uygulama ya da dış ortaklar için ise aynı modeller üzerine DRF API eklenir. Mülakatta "Django ile SPA" sorulursa CSRF'nin başlıkla gönderildiğini ve CORS'un ayrı bir konu olduğunu söyleyebilmelisin.
Uzmanlaş

Sorgu sayısını ölç, sonra optimize et

from django.db import connection ile len(connection.queries) (DEBUG açıkken) ya da testlerde self.assertNumQueries(2) kullan. only("ad", "fiyat") ve defer("aciklama") gereksiz sütunları çekmez; values_list("ad", flat=True) model nesnesi bile kurmaz. Büyük tabloda iterator() belleği korur; toplu yazmada bulk_create ve update() tek sorguyla çalışır.

İşlem bütünlüğü ve yarış durumları

Stok düşme gibi işlemleri transaction.atomic() içinde yap; iki kullanıcı aynı anda son ürünü alırken select_for_update() satırı kilitler. Urun.objects.filter(pk=5).update(stok=F("stok") - 1) ise okuma-yazma arasında kayıp güncellemeyi önler, çünkü hesap veritabanında yapılır.

Mimari alışkanlıklar

İş kuralını view'a gömme; model metotlarına ya da ayrı bir services.py katmanına koy. Sinyalleri (signals) ölçülü kullan; gizli yan etkiler hata ayıklamayı zorlaştırır. Ayarları django-environ ya da ortam değişkenleriyle yönet, pytest-django ile test yaz. Uzun süren işleri (e-posta, rapor) Celery gibi bir kuyruğa ver; bunun mantığını Orkestrasyon dersinde göreceksin.

Alıştırmalara geç (30)