Django Ninja¶
在上一個任務中,我們學習了如何使用 Django REST Framework 來建立 API。 現在,讓我們來認識另一個現代化的 API 框架——Django Ninja。
開始之前¶
任務目標
在這個任務中,你將學習:
- 了解 Django Ninja 的特色
- 使用 Django Ninja 建立第一個 API
- 理解 Schema 的用途並實作 CRUD 操作
- 使用 ModelSchema 簡化程式碼
- 使用 Router 組織 API 結構
- 設定 API 的驗證與授權機制
- 使用內建的 OpenAPI 文件
- 設定搜尋與過濾功能
什麼是 Django Ninja?¶
Django Ninja 是一個受 FastAPI 啟發的 Django API 框架。它的核心特色包括:
- 使用 Python Type Hints 定義 API 參數和回應
- 基於 Pydantic 的資料驗證
- 自動產生 OpenAPI 文件,不需要額外套件
- 高效能,支援非同步
- 語法簡潔,學習曲線平緩
安裝 Django Ninja¶
首先,讓我們安裝 Django Ninja:
Django Ninja 不需要加入 INSTALLED_APPS,直接使用即可。
建立第一個 API¶
Django Ninja 的架構設計是在專案層級建立一個 NinjaAPI 實例,然後讓各個 App 定義自己的 Router。這樣的設計讓程式碼更有組織性。
建立主要 API 實例¶
首先,在 core 資料夾中建立 ninja.py:
from ninja import NinjaAPI
api = NinjaAPI()
@api.get("/hello")
def hello(request):
return {"message": "Hello, Django Ninja!"}
設定 URL¶
接下來,在專案的主要 URL 設定中引入這個 API:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path, reverse_lazy
from django.views.generic import RedirectView
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
from core import views
from core.ninja import api as ninja_api
auth_urlpatterns = [
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path(
"logout/",
auth_views.LogoutView.as_view(),
name="logout",
),
path("register/", views.register, name="register"),
path(
"password-change/",
auth_views.PasswordChangeView.as_view(
template_name="registration/password_change.html",
success_url=reverse_lazy("auth:password_change_done"),
),
name="password_change",
),
path(
"password-change/done/",
auth_views.PasswordChangeDoneView.as_view(
template_name="registration/password_change_done.html"
),
name="password_change_done",
),
path(
"password-reset/",
auth_views.PasswordResetView.as_view(
template_name="registration/password_reset.html",
success_url=reverse_lazy("auth:password_reset_done"),
),
name="password_reset",
),
path(
"password-reset/done/",
auth_views.PasswordResetDoneView.as_view(
template_name="registration/password_reset_done.html"
),
name="password_reset_done",
),
path(
"password-reset/<uidb64>/<token>/",
auth_views.PasswordResetConfirmView.as_view(
template_name="registration/password_reset_confirm.html",
success_url=reverse_lazy("auth:password_reset_complete"),
),
name="password_reset_confirm",
),
path(
"password-reset/complete/",
auth_views.PasswordResetCompleteView.as_view(
template_name="registration/password_reset_complete.html"
),
name="password_reset_complete",
),
]
urlpatterns = [
path("", RedirectView.as_view(pattern_name="blog:article_list"), name="root"),
path("admin/", admin.site.urls),
path("practices/", include("practices.urls")),
path("blog/", include("blog.urls")),
path("auth/", include((auth_urlpatterns, "auth"))),
path("api-drf/blog/", include("blog.drf_urls")),
path("api-drf/token", obtain_auth_token, name="api-token"),
# API 文件
path("api-drf/schema", SpectacularAPIView.as_view(), name="schema"),
path(
"api-drf/docs",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
# Django Ninja API
path("api-ninja/", ninja_api.urls), # (1)!
]
if settings.DEBUG:
from debug_toolbar.toolbar import debug_toolbar_urls
urlpatterns = [
*urlpatterns,
*debug_toolbar_urls(),
*static(settings.STATIC_URL, document_root=settings.STATIC_ROOT),
*static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT),
]
- Django Ninja 的 URL 設定方式跟 DRF 不同,直接使用
api.urls即可
啟動開發伺服器後,開啟 http://127.0.0.1:8000/api-ninja/hello,你會看到 JSON 回應。
更棒的是,Django Ninja 內建了互動式 API 文件!開啟 http://127.0.0.1:8000/api-ninja/docs 就能看到自動產生的 Swagger UI 介面。
使用 Router 建立文章 API¶
在實際專案中,我們會讓各個 App 定義自己的 Router,然後在主要的 NinjaAPI 中註冊這些 Router。這樣可以保持程式碼的模組化。
建立 Router¶
在 blog 資料夾中建立 ninja.py:
檔案命名
這裡我們把檔案命名為 ninja.py,是為了與之前 DRF 的 drf_views.py 區分。在實際專案中,如果你只使用一種 API 框架,可以自由選擇檔案名稱,例如 routers.py、api.py 或 endpoints.py 都是常見的選擇。
from ninja import Router
from blog.models import Article
router = Router()
@router.get("/articles")
def list_articles(request):
articles = Article.objects.all()
return [
{
"id": article.id,
"title": article.title,
"content": article.content,
"is_published": article.is_published,
"created_at": article.created_at,
}
for article in articles
]
@router.get("/articles/{article_id}")
def get_article(request, article_id: int): # (1)!
article = Article.objects.get(id=article_id)
return {
"id": article.id,
"title": article.title,
"content": article.content,
"is_published": article.is_published,
"created_at": article.created_at,
}
- 路徑參數直接透過 Type Hint 定義類型,Django Ninja 會自動進行類型轉換和驗證
在主 API 中註冊 Router¶
更新 core/ninja.py,把 blog 的 Router 註冊進來:
from ninja import NinjaAPI
from blog.ninja import router as blog_router
api = NinjaAPI()
api.add_router("/blog", blog_router, tags=["文章"])
@api.get("/hello")
def hello(request):
return {"message": "Hello, Django Ninja!"}
現在 API 的結構變得更清晰了:
/api-ninja/blog/articles→ 文章列表/api-ninja/blog/articles/{article_id}→ 單一文章操作
這裡有幾個重點:
- 路徑參數:
{article_id}會自動對應到函式參數article_id - Type Hints:
article_id: int告訴 Django Ninja 這個參數應該是整數 - 自動驗證:如果傳入的不是有效的整數,會自動回傳 422 錯誤
Schema:資料的序列化與驗證¶
跟 DRF 的 Serializer 類似,Django Ninja 使用 Schema 來定義資料結構。Schema 基於 Pydantic,提供強大的資料驗證功能。
建立 Schema¶
建立一個新檔案 blog/schemas.py:
from datetime import datetime
from ninja import Schema
class ArticleIn(Schema):
title: str
content: str
is_published: bool = False # (1)!
class ArticleOut(Schema):
id: int
title: str
content: str
is_published: bool
created_by_id: int
author_id: int | None # (2)!
created_at: datetime
updated_at: datetime
- 設定預設值,如果沒有提供就使用
False - 使用
| None表示這個欄位可以是None
使用 Schema 改寫 API¶
from django.shortcuts import get_object_or_404
from ninja import Router
from blog.models import Article
from blog.schemas import ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut]) # (1)!
def list_articles(request):
return Article.objects.all() # (2)!
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id) # (3)!
response=list[ArticleOut]定義回應的資料結構- 直接回傳 QuerySet,Django Ninja 會自動轉換成 JSON
- 使用
get_object_or_404處理資料不存在的情況
使用 Schema 的好處:
- 自動序列化:直接回傳 Model instance 或 QuerySet,Schema 會自動轉換
- 自動驗證:輸入資料會根據 Schema 定義自動驗證
- 自動文件:Schema 會自動出現在 API 文件中
使用 ModelSchema 簡化¶
每次都要手動定義欄位很麻煩。Django Ninja 提供了 ModelSchema,可以自動根據 Model 產生 Schema:
from ninja import ModelSchema
from blog.models import Article
class ArticleIn(ModelSchema):
class Meta:
model = Article
fields = ["title", "content", "is_published"]
class ArticleOut(ModelSchema):
class Meta:
model = Article
fields = [
"id",
"title",
"content",
"is_published",
"created_by",
"author",
"created_at",
"updated_at",
]
ModelSchema 會自動:
- 根據 Model 的欄位定義產生對應的 Schema 欄位
- 設定適當的類型和驗證規則
- 處理
null=True的欄位
fields 的設定
跟 DRF 一樣,你也可以使用 fields = "__all__" 來包含所有欄位,但這不建議在正式環境中使用。
新增文章¶
接下來實作新增文章的 API:
from django.shortcuts import get_object_or_404
from ninja import Router
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut}) # (1)!
def create_article(request, payload: ArticleIn): # (2)!
article = Article.objects.create(**payload.dict())
return 201, article
response={201: ArticleOut}表示成功時回傳 201 狀態碼payload: ArticleIn表示請求的 body 會被解析成ArticleInSchema
更新文章¶
from django.shortcuts import get_object_or_404
from ninja import Router
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
部分更新文章¶
PUT 方法需要提供所有欄位,如果只想更新部分欄位,可以使用 PATCH 方法。Django Ninja 提供了 PatchDict 來簡化這個需求:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn], # (1)!
):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
PatchDict[ArticleIn]會自動將ArticleIn的所有欄位變成選填,並且只包含有提供的欄位
刪除文章¶
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
API 的驗證與授權¶
Django Ninja 提供了多種驗證方式,包括 Session 驗證、API Key、Bearer Token 等。
Session 驗證¶
最簡單的方式是使用 Django 內建的 Session 驗證:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from ninja.security import django_auth
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router(auth=django_auth) # (1)!
@router.get("/articles", response=list[ArticleOut])
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
- 在 Router 層級設定驗證,所有端點都會受到保護
現在所有端點都需要登入才能存取。如果想要讓某些端點公開存取,可以在個別端點上設定:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from ninja.security import django_auth
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router(auth=django_auth)
@router.get("/articles", response=list[ArticleOut], auth=None) # (1)!
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut, auth=None) # (2)!
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
auth=None表示這個端點不需要驗證auth=None表示這個端點不需要驗證
Bearer Token 驗證¶
如果你想要使用 Token 驗證,可以在 core/ninja.py 中自訂一個驗證類別:
from ninja import NinjaAPI
from ninja.security import HttpBearer
from rest_framework.authtoken.models import Token
from blog.ninja import router as blog_router
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
try:
token_obj = Token.objects.get(key=token)
except Token.DoesNotExist:
return None
return token_obj.user
api = NinjaAPI(auth=AuthBearer()) # (1)!
api.add_router("/blog", blog_router, tags=["文章"])
@api.get("/hello")
def hello(request):
return {"message": "Hello, Django Ninja!"}
- 在 API 層級設定全域驗證
複用 DRF 的 Token
這裡我們複用了 DRF 的 Token 模型(rest_framework.authtoken.models.Token),這樣就可以用同一個 Token 存取 DRF 和 Django Ninja 的 API。
在實務中通常不會安裝 DRF 可以使用其他第三方套件或自己實作。
接著為了方便測試讓我們把 Router 的驗證拿掉,因為 Router 的(範圍小的)會覆蓋範圍大的 auth 設定。
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut], auth=None)
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut, auth=None)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(**payload.dict())
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
使用方式跟 DRF 一樣,在請求標頭中加入 Authorization: Bearer <token>:
curl -X 'POST' \
'http://127.0.0.1:8000/api-ninja/blog/articles' \
-H "Authorization: Bearer 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"title": "Hello",
"content": "World",
"is_published": false
}'
取得登入使用者¶
在驗證通過後,你可以透過 request.auth 取得登入的使用者:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut], auth=None)
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut, auth=None)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
article = Article.objects.create(
**payload.dict(),
created_by=request.auth, # (1)!
)
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
request.auth就是驗證類別authenticate方法回傳的值
權限驗證¶
驗證(Authentication)確認使用者身份後,我們還需要授權(Authorization)來檢查使用者是否有權限執行某個操作。
Django Ninja 可以透過 ninja.errors.HttpError 來回傳權限錯誤,並結合 Django 內建的 has_perm 方法檢查使用者權限:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from ninja.errors import HttpError
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut], auth=None)
def list_articles(request):
return Article.objects.all()
@router.get("/articles/{article_id}", response=ArticleOut, auth=None)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
if not request.auth.has_perm("blog.add_article"): # (1)!
raise HttpError(403, "你沒有權限新增文章")
article = Article.objects.create(
**payload.dict(),
created_by=request.auth,
)
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
if not request.auth.has_perm("blog.change_article"): # (2)!
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
if not request.auth.has_perm("blog.delete_article"): # (3)!
raise HttpError(403, "你沒有權限刪除文章")
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
- 檢查使用者是否有
blog.add_article權限 - 檢查使用者是否有
blog.change_article權限 - 檢查使用者是否有
blog.delete_article權限
當權限驗證失敗時,API 會回傳 403 狀態碼和錯誤訊息:
使用內建的 OpenAPI 文件¶
Django Ninja 最大的優點之一就是內建 OpenAPI 文件,不需要安裝額外套件。
設定文件資訊¶
你可以在建立 NinjaAPI 時設定文件的基本資訊:
from ninja import NinjaAPI
from ninja.security import HttpBearer
from rest_framework.authtoken.models import Token
from blog.ninja import router as blog_router
class AuthBearer(HttpBearer):
def authenticate(self, request, token):
try:
token_obj = Token.objects.get(key=token)
except Token.DoesNotExist:
return None
return token_obj.user
api = NinjaAPI(
title="Blog API",
version="1.0.0",
description="Django 大冒險的部落格 API (Django Ninja 版本)",
auth=AuthBearer(),
)
api.add_router("/blog", blog_router, tags=["文章"])
@api.get("/hello")
def hello(request):
return {"message": "Hello, Django Ninja!"}
開啟 http://127.0.0.1:8000/api-ninja/docs 就能看到更新後的文件。
自訂 OpenAPI Schema¶
如果需要自訂 OpenAPI Schema,可以使用 openapi_extra 參數:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Router
from ninja.errors import HttpError
from blog.models import Article
from blog.schemas import ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut], auth=None)
def list_articles(request):
return Article.objects.all()
@router.get(
"/articles/{article_id}",
response=ArticleOut,
auth=None,
openapi_extra={
"responses": {
404: {
"description": "文章不存在",
}
}
},
)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
if not request.auth.has_perm("blog.add_article"):
raise HttpError(403, "你沒有權限新增文章")
article = Article.objects.create(
**payload.dict(),
created_by=request.auth,
)
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
if not request.auth.has_perm("blog.delete_article"):
raise HttpError(403, "你沒有權限刪除文章")
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
設定搜尋與過濾¶
Django Ninja 提供了 FilterSchema 來處理過濾邏輯。
基本過濾¶
from ninja import FilterSchema, ModelSchema
from blog.models import Article
class ArticleIn(ModelSchema):
class Meta:
model = Article
fields = ["title", "content", "is_published"]
class ArticleOut(ModelSchema):
class Meta:
model = Article
fields = [
"id",
"title",
"content",
"is_published",
"created_by",
"author",
"created_at",
"updated_at",
]
class ArticleFilterSchema(FilterSchema):
is_published: bool | None = None
title__icontains: str | None = None # (1)!
- 使用 Django ORM 的 lookup 語法,
__icontains表示不區分大小寫的包含搜尋
在 API 中使用:
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Query, Router
from ninja.errors import HttpError
from blog.models import Article
from blog.schemas import ArticleFilterSchema, ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut], auth=None)
def list_articles(request, filters: Query[ArticleFilterSchema]): # (1)!
return filters.filter(Article.objects.all()) # (2)!
@router.get(
"/articles/{article_id}",
response=ArticleOut,
auth=None,
openapi_extra={
"responses": {
404: {
"description": "文章不存在",
}
}
},
)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
if not request.auth.has_perm("blog.add_article"):
raise HttpError(403, "你沒有權限新增文章")
article = Article.objects.create(
**payload.dict(),
created_by=request.auth,
)
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
if not request.auth.has_perm("blog.delete_article"):
raise HttpError(403, "你沒有權限刪除文章")
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
Query[ArticleFilterSchema]表示這些參數來自查詢字串FilterSchema提供filter()方法來過濾 QuerySet
只顯示已發布的文章:
搜尋標題包含 "django" 的文章:
組合使用:
進階搜尋¶
如果你想要一個搜尋欄位同時搜尋多個欄位,可以使用 Field:
from typing import Annotated
from ninja import FilterLookup, FilterSchema, ModelSchema
from blog.models import Article
class ArticleIn(ModelSchema):
class Meta:
model = Article
fields = ["title", "content", "is_published"]
class ArticleOut(ModelSchema):
class Meta:
model = Article
fields = [
"id",
"title",
"content",
"is_published",
"created_by",
"author",
"created_at",
"updated_at",
]
class ArticleFilterSchema(FilterSchema):
is_published: bool | None = None
title__icontains: str | None = None
search: Annotated[str | None, FilterLookup(["title__icontains", "content__icontains"])] = None # (1)!
- 透過
FilterLookup指定要搜尋的欄位,會用 OR 條件組合
現在 search 參數會同時搜尋 title 和 content:
排序¶
Django Ninja 沒有內建的排序功能,但你可以輕鬆自己實作:
from typing import Literal
from django.shortcuts import get_object_or_404
from ninja import PatchDict, Query, Router
from ninja.errors import HttpError
from blog.models import Article
from blog.schemas import ArticleFilterSchema, ArticleIn, ArticleOut
router = Router()
@router.get("/articles", response=list[ArticleOut], auth=None)
def list_articles(
request,
filters: Query[ArticleFilterSchema],
ordering: Literal["created_at", "-created_at", "title", "-title"] | None = None, # (1)!
):
articles = filters.filter(Article.objects.all())
if ordering:
articles = articles.order_by(ordering)
return articles
@router.get(
"/articles/{article_id}",
response=ArticleOut,
auth=None,
openapi_extra={
"responses": {
404: {
"description": "文章不存在",
}
}
},
)
def get_article(request, article_id: int):
return get_object_or_404(Article, id=article_id)
@router.post("/articles", response={201: ArticleOut})
def create_article(request, payload: ArticleIn):
if not request.auth.has_perm("blog.add_article"):
raise HttpError(403, "你沒有權限新增文章")
article = Article.objects.create(
**payload.dict(),
created_by=request.auth,
)
return 201, article
@router.put("/articles/{article_id}", response=ArticleOut)
def update_article(request, article_id: int, payload: ArticleIn):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.dict().items():
setattr(article, attr, value)
article.save()
return article
@router.patch("/articles/{article_id}", response=ArticleOut)
def partial_update_article(
request,
article_id: int,
payload: PatchDict[ArticleIn],
):
if not request.auth.has_perm("blog.change_article"):
raise HttpError(403, "你沒有權限編輯文章")
article = get_object_or_404(Article, id=article_id)
for attr, value in payload.items():
setattr(article, attr, value)
article.save()
return article
@router.delete("/articles/{article_id}", response={204: None})
def delete_article(request, article_id: int):
if not request.auth.has_perm("blog.delete_article"):
raise HttpError(403, "你沒有權限刪除文章")
article = get_object_or_404(Article, id=article_id)
article.delete()
return 204, None
- 使用
Literal限制可用的排序選項
任務結束¶
完成!
恭喜你完成了這個任務!現在你已經學會:
- 了解 Django Ninja 的特色
- 使用 Django Ninja 建立第一個 API
- 理解 Schema 的用途並實作 CRUD 操作
- 使用 ModelSchema 簡化程式碼
- 使用 Router 組織 API 結構
- 設定 API 的驗證與授權機制
- 使用內建的 OpenAPI 文件
- 設定搜尋與過濾功能