汎用ビュー

revision-up-to:17812 (1.4)
リリースノートを参照してください

Note

Django 1.3 から、クラスベースの汎用ビューが採用されたので、関数ベースの汎用 ビューは廃止されました。クラスベースビューの トピックガイド詳細なリファレンス に記述されています。

Web アプリケーションの作成は、同じパターンを何度も何度も繰り返し書くことに なるため、退屈なものです。 Django は、こうした単調作業をモデルやテンプレー トのレイヤで軽減しようとしていますが、 Web 開発者はビューレベルでも退屈さを 感じています。

Django の 汎用ビュー (generic views) はその苦労を無くすために開発されま した。ビューの開発時に見かける、特定の共通するイディオムとパターンを汎用ビュー という形で抽象化しています。余計なコードを書かずによく定義されるビューを素早く 実装できます。

我々はオブジェクトのリスト表示などの、ありふれたタスクを知っています。 任意の オブジェクトをリスト表示するコードも提供できます。その上、リスト表示させたい モデルは URLconf に追加の引数として渡せます。

Django の汎用ビューで以下のようなことができます:

  • 別のページへリダイレクトし、与えられたテンプレートをレンダリングするような、 よくある簡単なタスクの処理。
  • リストと単一オブジェクトの詳細ページを表示。カンファレンスを管理するアプリケー ションを作っているなら、 talk_list ビューと registerd_user_list ビュー にリストビューを、単一の発表に関するページに詳細ビューが使えます。
  • 日付ベースのオブジェクトを年/月/日のアーカイブで表示。詳細と最新ページで 関連付けて表示します。 Django のブログ (http://www.djangoproject.com/weblog/) の年月日ごとのアーカイブはこれで作られています。
  • 認証あり、または無しでユーザにオブジェクトを生成、更新、削除を許可。

汎用ビューは開発者が遭遇する、よくあるタスクを果たすための簡単なインターフェース を提供しています。

汎用ビューを使う

これらビューはすべて、 URLconf ファイルに設定の入った辞書を作成し、 URLconf で URL パターンを記述しているタプルの 3 つめのメンバにその辞書を渡せば使えます。

例えばここでは、静的な “about” ページを提示するために使えるシンプルな URLconf を 示します:

from django.conf.urls import patterns, url, include
from django.views.generic.simple import direct_to_template

urlpatterns = patterns('',
    ('^about/$', direct_to_template, {
        'template': 'about.html'
    })
)

これは一見、ちょっとした “魔法” のように見えるかもしません。だってほら、コー ド無しのビューですよ!実際には direct_to_template ビューは単に、特別なパ ラメータの入った辞書から情報を取得し、レンダリングに使用しています。

汎用ビューは独自のビュー内で再利用できます。汎用ビューは、その他ビューと同じよう に普通のビュー関数だからです。例として、 “about” を拡張して静的にレンダリングさ れた about/<whatever>.html にURL /about/<whatever>/ を対応させてみましょ う。まずは、ビュー関数を指定するように URLconf を変更します:

from django.conf.urls import patterns, url, include
from django.views.generic.simple import direct_to_template
from books.views import about_pages

urlpatterns = patterns('',
    ('^about/$', direct_to_template, {
        'template': 'about.html'
    }),
    ('^about/(\w+)/$', about_pages),
)

次に about_pages ビューを書きましょう:

from django.http import Http404
from django.template import TemplateDoesNotExist
from django.views.generic.simple import direct_to_template

def about_pages(request, page):
    try:
        return direct_to_template(request, template="about/%s.html" % page)
    except TemplateDoesNotExist:
        raise Http404()

ここでは direct_to_template をほかの関数と同じように扱っています。 direct_to_templateHttpResponse をそのまま返すので、戻り値を単純にそ のまま返せます。唯一トリッキーでやっかいなのが、存在しないテンプレートを扱ってい る点です。存在しないテンプレートがサーバエラーを引き起こさないように TemplateDoesNotExist 例外をキャッチし、代わりに404エラーを返しています。

セキュリティ上の脆弱性はありますか?

目ざとい読者ならセキュリティホールの可能性に気がついてるかもしれません。ブラ ウザから補間されたコンテンツを使ってテンプレート名を構築しているからです (template="about/%s.html" % page) 。一見すると、古典的な ディレクトリト ラバーサル の脆弱性のようです。しかし本当でしょうか?

そうではありません。たしかに、悪意をもって作成された page の値なら、 ディレクトリトラバーサルを引き起こす可能性があります。しかし、 page の値はリクエストURLから取得されますが、必ずしもすべての値が受け入れられるわ けではありません。鍵は URLconf にあります。正規表現 \w+page の URL の一部とマッチさせるために使っています。 \w はアルファベットと数字の み受け付けます。なので、悪意ある文字列 (ここではドットやスラッシュなど) は ビュー自体に到達する前に、 URL リゾルバに拒否されます。

オブジェクトの汎用ビュー

direct_to_template は確かに便利です。しかし、 Django の汎用ビューが真価を 発揮するのは、データベースの内容についてビューを提示するときです。驚くほど簡単に オブジェクトのリスト、詳細ビューを生成できる組み込みの汎用ビューが、 Django には少数付属しています。それらはよくあるタスクだからです。

それでは汎用ビューの 1 つ、 “オブジェクトリスト (object list)” ビューを見てみま しょう。以下のようなモデルを使います:

# models.py
from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=30)
    address = models.CharField(max_length=50)
    city = models.CharField(max_length=60)
    state_province = models.CharField(max_length=30)
    country = models.CharField(max_length=50)
    website = models.URLField()

    def __unicode__(self):
        return self.name

    class Meta:
        ordering = ["-name"]

class Book(models.Model):
    title = models.CharField(max_length=100)
    authors = models.ManyToManyField('Author')
    publisher = models.ForeignKey(Publisher)
    publication_date = models.DateField()

全ての出版社 (publisher) のリストページを構築するため、以下のような URLconf を 使います:

from django.conf.urls import patterns, url, include
from django.views.generic import list_detail
from books.models import Publisher

publisher_info = {
    "queryset" : Publisher.objects.all(),
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info)
)

書くべき Python コードは以上です。しかしテンプレートも書く必要があります。追加の 引数の辞書に template_name キーを含めることにより、どのテンプレートを使用す るべきであるか object_list ビューに明示的に伝えることができます。明示的なテ ンプレートがない場合 Django は、オブジェクトの名前からテンプレートを推論します。 今回の場合、推論されるテンプレートは "books/publisher_list.html" となるで しょう。 “books” という部分はモデルを定義しているアプリケーションの名前からきて います。 “publisher” は単にモデル名が lowercase になったものです。

このテンプレートは、 object_list 変数を含むコンテキストに対してレンダリング されます。 object_list はすべての Publisher オブジェクトからなります。とても シンプルなテンプレートにするなら以下のようになるでしょう:

{% extends "base.html" %}

{% block content %}
    <h2>Publishers</h2>
    <ul>
        {% for publisher in object_list %}
            <li>{{ publisher.name }}</li>
        {% endfor %}
    </ul>
{% endblock %}

本当にこれだけです。汎用ビューに渡される “info” 辞書を変更することで、クールな 機能をすべて使用できます。すべての汎用ビューとオプションの詳細は generic views reference に記されています。このドキュ メントの残りでは、よくある汎用ビューのカスタマイズ、拡張の方法を記しています。

汎用ビューの拡張

疑いようも無く、汎用ビューを使用すると開発スピードは大幅に上がります。しかし多 くのプロジェクトでは、汎用ビューでは物足りないときがあります。確かに、新米の Django 開発者からくる質問で典型的なのは、汎用ビューをより幅広い状況で扱うことに ついてです。

幸いほぼすべてのケースで、汎用ビューを簡単に拡張して、より大きいユースケースを 処理するようにできます。通常これらの状況は、以下のセクションで扱うごく少数の パターンに分類できます。

“フレンドリー” なテンプレートコンテキストを作る

サンプルの出版社リストテンプレートでは、 object_list 変数にすべての 書籍 (book) オブジェクトが格納されていると思ってしまうかもしれません。現状でも うまく動作しますが、テンプレートの作者にとって “フレンドリー” ではありません。 テンプレート作者は、ここでは出版社のことだけを知っているべきです。より良い変数名は publisher_list です。変数の内容がかなり明らかになっています。

template_object_name 引数を変更することで、簡単に object_list の名前を 変えられます:

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info)
)

便利な template_object_name を提供するのは常によいことです。テンプレートの デザインを担当する同僚は、あなたに感謝することでしょう。

追加のコンテキスト

汎用ビューで提供されるもの以上に、追加の情報を提示したいときがあります。例えば、 各出版社の詳細ページに、書籍の全リストを表示したいときなどです。汎用ビュー object_detail はコンテキストに Publisher オブジェクトを提供していますが、 テンプレートに追加の情報を与えることはできなさそうです。

しかしそうではありません。すべての汎用ビューは追加のパラメータ extra_context をとります。これは追加オブジェクトの辞書で、テンプレートコンテキストに追加され ます。書籍の全リストを詳細ビューに提供するには、以下のように info 辞書を使い ます。

from books.models import Publisher, Book

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : {"book_list" : Book.objects.all()}
}

これでテンプレートコンテキストで {{ book_list }} が使えるようになります。 このパターンは、汎用ビューのテンプレートに任意の情報を渡すために使えます。非常に 便利です。

しかし実際には、微妙なバグがあります。見つけられました?

その問題は extra_context 中のクエリが評価されるときに関係します。この例で は、 URLconf に Book.objects.all() を置いているので、クエリは 1 度だけ (URLconf が最初にロードされるとき) 評価されます。書籍を追加か削除すれば、 Web サーバをリロードするまで変更が反映されません。 (クエリセットが、いつキャッ シュ、評価されるかついては キャッシュとクエリセット を参照してください) 。

Note

この問題は汎用ビューの queryset 引数には当てはまりません。 特定の QuerySet は 絶対に キャッシュすべきでないと Django は知っています。 汎用ビューはビューがそれぞれレンダリングされるときに、キャッシュを消去して くれます。

解決策は、値の代わりに extra_context でコールバックを使うことです。 extra_context に渡される呼び出し可能オブジェクト (つまり関数) は、ビューが レンダリングされたときに (1 度だけではなく) 評価されます。明示的に定義した関数を 使います:

def get_books():
    return Book.objects.all()

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : {"book_list" : get_books}
}

明示性に欠けるも、短く書く方法もあります。 Book.objects.all 自体が呼び出し 可能なので、それを利用します:

publisher_info = {
    "queryset" : Publisher.objects.all(),
    "template_object_name" : "publisher",
    "extra_context" : {"book_list" : Book.objects.all}
}

Book.objects.all の後ろに丸括弧が足りませんね。実際に関数は呼び出されて いません (汎用ビューが後で呼び出します)。

オブジェクトのサブセットを表示

さて、ずっと使ってきた queryset キーを詳しく見ていきましょう。ほとんどの 汎用ビューは queryset 引数を取ります。それはビューがどのオブジェクト のセットを表示すべきかを指定します。 ( QuerySet オブジェクトのについては クエリを生成する 、詳細は 汎用ビューのリファレンス を参照してください)。

簡単な例として、出版日 (publication date) 順の書籍のリストを最新のものから取得 してみます:

book_info = {
    "queryset" : Book.objects.all().order_by("-publication_date"),
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    (r'^books/$', list_detail.object_list, book_info),
)

かなり簡単な例ですが、うまく考えを表しています。もちろんオブジェクトを再整理する 以上のことをしたいでしょう。特定の出版社についての書籍を渡したい場合も、同じ テクニックが使えます:

acme_books = {
    "queryset": Book.objects.filter(publisher__name="Acme Publishing"),
    "template_name" : "books/acme_list.html"
}

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    (r'^books/acme/$', list_detail.object_list, acme_books),
)

フィルタされた queryset 以外に、テンプレート名を指定しています。そうしな いと、汎用ビューは同じテンプレートを “ありきたりな” オブジェクトリストとして使い 回します。たぶん期待とは違うことでしょう。

これは、ある出版社固有の書籍を扱ううえでエレガントな方法ではありません。他の 出版社のページを追加したいなら、 URLconf に手動で追記する必要があるからです。 追記したうちのいくつかは不当 (unreasonable) になるでしょう。詳しくは次のセク ションで扱います。

Note

/books/acme/ をリクエストするの際に 404 になったときは、 ‘ACME Publishing’ という名の Publisher が実際に存在するか確かめてください。 こういったケースに備えて、汎用ビューは allow_empty というパラメータを 持っています。詳細は 汎用ビューのリファレンス を参照してください。

ラッパー関数で多重フィルタリング

他によくあるニーズは、 URL のキーでオブジェクトをフィルタしてから、オブジェクト をリストページに渡すことです。すでに出版社名は URLconf にハードコードしてい ますが、ある任意の出版社について書籍をすべて表示するビューを書きたいなら、どう しますか? object_list 汎用ビューを “ラップ (wrap)” して大量のコードを手書き することを避けられます。いつも通り、 URLconf から書きます:

from books.views import books_by_publisher

urlpatterns = patterns('',
    (r'^publishers/$', list_detail.object_list, publisher_info),
    (r'^books/(\w+)/$', books_by_publisher),
)

次に、 books_by_publisher ビュー自体を書きます:

from django.http import Http404
from django.views.generic import list_detail
from books.models import Book, Publisher

def books_by_publisher(request, name):

    # publisher を取得 (見つからなければ 404 エラーを返します)。
    try:
        publisher = Publisher.objects.get(name__iexact=name)
    except Publisher.DoesNotExist:
        raise Http404

    # 重労働には object_list view ビューを使います。
    return list_detail.object_list(
        request,
        queryset = Book.objects.filter(publisher=publisher),
        template_name = "books/books_by_publisher.html",
        template_object_name = "books",
        extra_context = {"publisher" : publisher}
    )

汎用ビューはとくに特別なものでもないので、これで動作します。単なる Python の関数 です。他のビューのように、汎用ビューは引数のセットを期待し、 HttpResponse オブジェクトを返します。このように、汎用ビューを小さな関数でラップするのは非常に 簡単です。ラッパー関数は、汎用ビューに引き渡す前 (もしくは後。詳細は次のセクション で) に追加の処理をします。

Note

先の例では、表示されている Publisher オブジェクトを extra_context に 渡しています。この性質のラッパーにおいては良いアイディアです。どの “親” オブジェクトが現在閲覧されているかをテンプレートに知らせられるかれです。

追加の処理をさせる

最後の共通パターンは、汎用ビューを呼び出す前か後に、追加の処理をさせることです。

Author オブジェクト中に last_accessed フィールドがあると想像してください。 著者 (author) が最後に問い合わせされた時間を記録します:

# models.py

class Author(models.Model):
    salutation = models.CharField(max_length=10)
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=40)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='/tmp')
    last_accessed = models.DateTimeField()

object_detail 汎用ビューはもちろん、このフィールドについては知りません。 しかし再び、このフィールドの更新を維持するためのカスタムビューを書けます。

初めに、カスタムビューを指定するために URLconf に著者の詳細に関する 1 行を追加 します:

from books.views import author_detail

urlpatterns = patterns('',
    #...
    (r'^authors/(?P<author_id>\d+)/$', author_detail),
)

そしてラッパー関数を書きます:

import datetime
from books.models import Author
from django.views.generic import list_detail
from django.shortcuts import get_object_or_404

def author_detail(request, author_id):
    # Author を取得 (見つからなければ 404 エラーを返します)。
    author = get_object_or_404(Author, pk=author_id)

    # 最終アクセス日時を記録します。
    author.last_accessed = datetime.datetime.now()
    author.save()

    # 詳細ページを表示します。
    return list_detail.object_detail(
        request,
        queryset = Author.objects.all(),
        object_id = author_id,
    )

Note

実際には、このコードには books/author_detail.html テンプレートが必要 です。

汎用ビューのレスポンスを変更するために、同じイディオムが使えます。著者リストの プレーンテキスト版をダウンロード可能にするには、以下のようなビューが使えます:

def author_list_plaintext(request):
    response = list_detail.object_list(
        request,
        queryset = Author.objects.all(),
        mimetype = "text/plain",
        template_name = "books/author_list.txt"
    )
    response["Content-Disposition"] = "attachment; filename=authors.txt"
    return response

これで動作するのは、汎用ビューがシンプルな HttpResponse オブジェクトを返す からです。 HttpResponse オブジェクトを辞書のように扱って、 Http セッダを設定 できます。この Content-Disposition の編集によって、ブラウザにページ表示する 代わりに、ダウンロードと保存するよう指示します。