Django基礎 #4 URL と Views (FBV)

読了 6分

#3 Models と ORM 基礎Post モデルとデータを揃えました。今回はそのデータを URL で公開 し、view 関数がどのようにレスポンスを作るかを見ます。関数ベースビュー (FBV) から — クラスベースビュー (CBV) は 中級 #1 で。

URLconf — URL と view のマッピング #

Django の URL ルーティングは urls.py が担当します。1 ファイルの urlpatterns リストがパターンを集めたものです。

config/urls.py — 最上位
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("blog/", include("blog.urls")),
]
blog/urls.py — アプリ別
from django.urls import path

from . import views

app_name = "blog"

urlpatterns = [
    path("", views.post_list, name="post_list"),
    path("<int:post_id>/", views.post_detail, name="post_detail"),
    path("new/", views.post_new, name="post_new"),
    path("<int:post_id>/edit/", views.post_edit, name="post_edit"),
]

3 つの主要関数:

  • path(route, view, name=...) — URL パターン 1 行
  • include("blog.urls") — 別モジュールの urlpatterns を差し込む
  • app_name = "blog" — 名前空間。別アプリと URL 名が衝突するのを防ぐ

URL パラメータ — <int:...><slug:...> #

path() の第一引数に <タイプ:変数名> を書くと view 関数の引数として渡されます。

path converters
urlpatterns = [
    path("<int:post_id>/", views.post_detail, name="post_detail"),
    path("category/<slug:slug>/", views.by_category, name="by_category"),
    path("year/<int:year>/month/<int:month>/", views.archive, name="archive"),
    path("uuid/<uuid:token>/", views.by_token, name="by_token"),
]

デフォルトで提供される converter:

タイプマッチ
strスラッシュ以外のすべての文字列 (デフォルト)
int正の整数
slug[-a-zA-Z0-9_]+ (URL フレンドリーな文字列)
uuidUUID 形式
pathスラッシュを含むすべての文字列

これらのパラメータが view 関数の キーワード引数 として渡されます。

blog/views.py
def post_detail(request, post_id):
    ...

def archive(request, year, month):
    ...

名前が正確に一致する必要があります — <int:post_id> なら関数の引数も post_id

関数ベースビュー (FBV) #

Django の view は リクエストを受け取りレスポンスを返す関数 です。もっとも単純な形:

blog/views.py — もっとも小さい view
from django.http import HttpResponse


def index(request):
    return HttpResponse("Hello, blog!")

ルールは 2 つ — 第一引数が request、戻り値が HttpResponse (またはそのサブクラス)。

request オブジェクト #

request (正確には HttpRequest インスタンス) が持っているもの:

request のよく使う属性
def example(request):
    request.method           # "GET", "POST", "PUT", "DELETE", ...
    request.GET              # ?key=value のクエリパラメータ (QueryDict)
    request.POST             # POST 本文 (フォームデータ)
    request.FILES            # アップロードされたファイル
    request.COOKIES          # クッキー dict
    request.session          # セッション dict (#7)
    request.user             # ログイン済みユーザー (#7)
    request.headers          # リクエストヘッダ
    request.path             # "/blog/1/" のようなパス
    ...

HttpResponse — もっとも基本のレスポンス #

HttpResponse
from django.http import HttpResponse


def hello(request):
    return HttpResponse("Hello!", content_type="text/plain")


def html(request):
    return HttpResponse("<h1>こんにちは</h1>")  # デフォルトは text/html


def with_status(request):
    return HttpResponse("Forbidden", status=403)

ステータスコード / ヘッダなどを直接扱うとき。

JsonResponse — JSON レスポンス #

JsonResponse
from django.http import JsonResponse


def post_list_json(request):
    posts = [
        {"id": 1, "title": "最初の記事"},
        {"id": 2, "title": "2番目の記事"},
    ]
    return JsonResponse({"items": posts})

dict を自動で JSON に変換してくれます。safe=False を渡せば list も直接レスポンス可能。ただし本格的な JSON API なら DRF が正解です。

render — テンプレート + コンテキストで HTML #

もっともよく使うレスポンスヘルパー。

render
from django.shortcuts import render

from .models import Post


def post_list(request):
    posts = Post.objects.filter(is_published=True).order_by("-created_at")
    return render(request, "blog/post_list.html", {"posts": posts})

3 つの引数:

  1. request
  2. テンプレートのパス (アプリの templates/ ディレクトリ基準)
  3. コンテキスト dict (テンプレートで {{ posts }} としてアクセス)

テンプレートファイルを作るのは #5 で詳しく。今回は view の形に集中します。

get_object_or_404 — なければ 404 #

Post.objects.get(pk=1) はオブジェクトがなければ Post.DoesNotExist 例外を投げます。これを毎回 try/except で囲まずに get_object_or_404 を使ってください。

get_object_or_404
from django.shortcuts import get_object_or_404, render

from .models import Post


def post_detail(request, post_id):
    post = get_object_or_404(Post, pk=post_id, is_published=True)
    return render(request, "blog/post_detail.html", {"post": post})

オブジェクトがなければ自動で HTTP 404 レスポンスを作ってくれます。追加条件 (is_published=True のような) も一緒に渡せます。

QuerySet 用には同じく get_list_or_404 があります — 空のリストなら 404。

request.method 分岐 — GET / POST #

フォームのある view のよくあるパターン。

GET / POST 分岐
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse

from .models import Post


def post_new(request):
    if request.method == "POST":
        title = request.POST.get("title", "").strip()
        content = request.POST.get("content", "").strip()
        if not title:
            return render(request, "blog/post_form.html", {"error": "タイトルを入力してください"})
        post = Post.objects.create(title=title, content=content, author=request.user)
        return HttpResponseRedirect(reverse("blog:post_detail", args=[post.id]))

    # GET — 空のフォーム
    return render(request, "blog/post_form.html")

このパターンは毎回手で書くと面倒です — Django の Form がこれを抽象化します (#6)。

Named URL — name=... がある理由 #

URL をハードコードするのは悪い習慣です。/blog/1/ のようなパスが変わるとコード / テンプレート両方をいちいち直さなければなりません。名前 で参照してください。

reverse
from django.urls import reverse

reverse("blog:post_list")                          # "/blog/"
reverse("blog:post_detail", args=[42])             # "/blog/42/"
reverse("blog:post_detail", kwargs={"post_id": 42}) # "/blog/42/"

<app_name>:<url_name> 形式。urls.pyapp_name = "blog" + path(..., name="post_detail")"blog:post_detail" を作ります。

テンプレートでは {% url %} #

テンプレートで
<a href="{% url 'blog:post_detail' post.id %}">{{ post.title }}</a>

redirect — reverse の短縮版 #

redirect
from django.shortcuts import redirect

def post_new(request):
    if request.method == "POST":
        post = Post.objects.create(...)
        return redirect("blog:post_detail", post_id=post.id)
    return render(request, "blog/post_form.html")

redirect は内部的に reverse + HttpResponseRedirect を一度にしてくれます。URL 名、パス文字列、モデルインスタンス (モデルに get_absolute_url を定義した場合) いずれも受け取ります。

get_absolute_url — モデル自身の URL #

モデルによく追加されるメソッド。

blog/models.py
from django.db import models
from django.urls import reverse


class Post(models.Model):
    title = models.CharField(max_length=200)
    # ...

    def get_absolute_url(self) -> str:
        return reverse("blog:post_detail", args=[self.pk])

この 1 行があれば:

  • redirect(post) が自動で動作
  • Admin の「サイトで見る」リンクが生きる
  • テンプレートで <a href="{{ post.get_absolute_url }}"> として使える

POST と CSRF #

Django はすべての POST リクエストに CSRF トークン を要求します (セキュリティのデフォルト値)。フォームテンプレートの中に {% csrf_token %} を入れる必要があります — 詳しくは #6

API のように CSRF が必要ない場合はデコレータで除外:

csrf_exempt (慎重に)
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook(request):
    ...

csrf_exempt外部 webhook のように本当に必要な箇所だけに。ユーザーフォームには使ってはいけません。

HTTP メソッド制限 — require_http_methods #

view が受け付けるメソッドを明示的に制限。

メソッド制限
from django.views.decorators.http import require_GET, require_POST, require_http_methods


@require_GET
def post_list(request):
    ...


@require_POST
def post_delete(request, post_id):
    ...


@require_http_methods(["GET", "POST"])
def post_new(request):
    ...

許可されていないメソッドであれば 405 Method Not Allowed を自動レスポンス。

小さな総合例 #

ブログ view 4 つを一カ所に:

blog/views.py — 総合
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_http_methods

from .models import Post


def post_list(request):
    qs = Post.objects.filter(is_published=True).order_by("-created_at")
    q = request.GET.get("q")
    if q:
        qs = qs.filter(title__icontains=q)
    return render(request, "blog/post_list.html", {"posts": qs, "q": q or ""})


def post_detail(request, post_id):
    post = get_object_or_404(Post, pk=post_id, is_published=True)
    return render(request, "blog/post_detail.html", {"post": post})


@login_required
@require_http_methods(["GET", "POST"])
def post_new(request):
    if request.method == "POST":
        title = request.POST.get("title", "").strip()
        content = request.POST.get("content", "").strip()
        if title and content:
            post = Post.objects.create(
                title=title,
                content=content,
                author=request.user,
            )
            return redirect("blog:post_detail", post_id=post.id)
    return render(request, "blog/post_form.html")


@login_required
@require_POST
def post_delete(request, post_id):
    post = get_object_or_404(Post, pk=post_id, author=request.user)
    post.delete()
    return redirect("blog:post_list")

@login_required はログインしていないユーザーをログインページへリダイレクトします — #7 で。

FBV vs CBV — プレビュー #

Django は クラスベースビュー (CBV) も提供します。

🚫 まだ扱わない — CBV プレビュー
from django.views.generic import ListView, DetailView


class PostListView(ListView):
    model = Post
    template_name = "blog/post_list.html"
    context_object_name = "posts"


class PostDetailView(DetailView):
    model = Post

CBV は 繰り返しパターンをクラスで縮めるツール ですが、最初は FBV のほうが直感的です。小さなプロジェクトなら FBV だけで十分です。CBV の深さは 中級 #1 で別途扱います。

まとめ #

今回つかんだもの:

  • URLconf — path()include()app_name でモジュール化
  • URL パラメータ — <int:><slug:><uuid:><path:>
  • FBV — def view(request, ...) -> HttpResponse
  • request.methodrequest.GETrequest.POSTrequest.user
  • HttpResponseJsonResponserender
  • get_object_or_404 — なければ自動で 404
  • Named URL — app:name + reverse / {% url %} / redirect
  • get_absolute_url のコンベンション
  • CSRF はデフォルト、フォームに {% csrf_token %} 必須
  • require_GET / POST / http_methods でメソッド制限
  • CBV は 中級 #1

次回(#5 Templates と静的ファイル)では view が返した render(...) が実際の HTML になるところ — テンプレート構文、継承、静的ファイル (CSS、JS、画像) まで扱います。

X