Django の ListView と DetailView でよくある一覧・詳細画面を作る

一覧画面と詳細画面は Web アプリケーションで最も頻繁に登場するパターンです。Django の汎用クラスベースビュー ListViewDetailView を使えば、この 2 つの画面をわずか数行で実装できます。

ListView で一覧画面を作る

ListView はモデルのレコード一覧を取得してテンプレートに渡すビューです。

from django.views.generic import ListView
from .models import Article

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'

これだけで、Article テーブルの全レコードを取得し、テンプレート変数 articles として渡す処理が完成します。

template_name を省略すると、Django は アプリ名/モデル名_list.html(例: articles/article_list.html)を自動的に探しにいきます。context_object_name を省略した場合は object_list という名前になります。明示的に指定しておく方が、テンプレート側で何のデータかわかりやすくなります。

テンプレートはシンプルなループで書けます。

{% extends "base.html" %}

{% block content %}
<h1>記事一覧</h1>
{% for article in articles %}
    <div>
        <h2><a href="{% url 'article_detail' article.pk %}">{{ article.title }}</a></h2>
        <p>{{ article.body|truncatechars:100 }}</p>
    </div>
{% empty %}
    <p>記事がありません。</p>
{% endfor %}
{% endblock %}

{% empty %}for ループの結果が 0 件だったときに表示されるブロックです。

queryset で表示対象を絞り込む

全件ではなく条件を絞りたい場合は queryset を上書きします。

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'
    queryset = Article.objects.filter(is_draft=False).order_by('-published_at')

さらに動的な条件(URL パラメータに応じた絞り込みなど)が必要なら、get_queryset メソッドをオーバーライドします。

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'

    def get_queryset(self):
        qs = Article.objects.filter(is_draft=False)
        category = self.request.GET.get('category')
        if category:
            qs = qs.filter(category__name=category)
        return qs

ページネーション

paginate_by を設定するだけで、自動的にページ分割されます。

class ArticleListView(ListView):
    model = Article
    template_name = 'articles/list.html'
    context_object_name = 'articles'
    paginate_by = 10

テンプレート側では page_obj を使ってページ送りの UI を構築します。

{% if page_obj.has_previous %}
    <a href="?page={{ page_obj.previous_page_number }}">前へ</a>
{% endif %}

<span>{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>

{% if page_obj.has_next %}
    <a href="?page={{ page_obj.next_page_number }}">次へ</a>
{% endif %}

ページネーションの処理を自分で書くと、境界値のチェックやクエリパラメータの処理でコードが膨らみがちですが、ListView なら paginate_by の 1 行で済みます。

DetailView で詳細画面を作る

DetailView は URL から主キー(pk)やスラッグを受け取り、該当する 1 件のレコードをテンプレートに渡します。

from django.views.generic import DetailView

class ArticleDetailView(DetailView):
    model = Article
    template_name = 'articles/detail.html'
    context_object_name = 'article'

URL の設定で <int:pk> を使い、主キーをビューに渡します。

from django.urls import path
from .views import ArticleListView, ArticleDetailView

urlpatterns = [
    path('articles/', ArticleListView.as_view(), name='article_list'),
    path('articles/<int:pk>/', ArticleDetailView.as_view(), name='article_detail'),
]

該当するレコードが存在しない場合、DetailView は自動的に 404 ページを返します。get_object_or_404 を自分で呼ぶ必要はありません。

ListView

モデルの一覧を取得してテンプレートに渡す。paginate_by でページ分割も可能

DetailView

URL パラメータから 1 件を特定してテンプレートに渡す。存在しなければ自動で 404

get_context_data で追加のコンテキストを渡す

テンプレートにモデルのデータ以外の情報も渡したい場合は get_context_data をオーバーライドします。

class ArticleDetailView(DetailView):
    model = Article
    template_name = 'articles/detail.html'
    context_object_name = 'article'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['related_articles'] = Article.objects.filter(
            category=self.object.category
        ).exclude(pk=self.object.pk)[:5]
        return context

super().get_context_data(**kwargs) で元のコンテキスト(article オブジェクト等)を引き継ぎ、そこに related_articles を追加しています。self.object で現在表示中のレコードにアクセスできる点も覚えておくと便利です。

ListView と DetailView のまとめ

ListViewDetailView は、Django アプリケーションで最も使用頻度の高い汎用ビューです。基本的な使い方はモデルとテンプレートを指定するだけ、カスタマイズが必要になったら get_querysetget_context_dataget_object といったメソッドをオーバーライドする——この段階的なアプローチが汎用ビューの設計思想になっています。

まずはシンプルな設定で動かし、要件に応じてメソッドを上書きしていくのが、汎用ビューを効率よく使いこなすコツです。