revision-up-to: | 17812 (1.4) |
---|
リリースノートを参照してください
Note
Django 1.3 から、クラスベースの汎用ビューが採用されたので、関数ベースの汎用 ビューは廃止されました。クラスベースビューの トピックガイド と 詳細なリファレンス に記述されています。
Web アプリケーションの作成は、同じパターンを何度も何度も繰り返し書くことに なるため、退屈なものです。 Django は、こうした単調作業をモデルやテンプレー トのレイヤで軽減しようとしていますが、 Web 開発者はビューレベルでも退屈さを 感じています。
Django の 汎用ビュー (generic views) はその苦労を無くすために開発されま した。ビューの開発時に見かける、特定の共通するイディオムとパターンを汎用ビュー という形で抽象化しています。余計なコードを書かずによく定義されるビューを素早く 実装できます。
我々はオブジェクトのリスト表示などの、ありふれたタスクを知っています。 任意の オブジェクトをリスト表示するコードも提供できます。その上、リスト表示させたい モデルは URLconf に追加の引数として渡せます。
Django の汎用ビューで以下のようなことができます:
talk_list
ビューと registerd_user_list
ビュー
にリストビューを、単一の発表に関するページに詳細ビューが使えます。汎用ビューは開発者が遭遇する、よくあるタスクを果たすための簡単なインターフェース を提供しています。
これらビューはすべて、 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_template
は HttpResponse
をそのまま返すので、戻り値を単純にそ
のまま返せます。唯一トリッキーでやっかいなのが、存在しないテンプレートを扱ってい
る点です。存在しないテンプレートがサーバエラーを引き起こさないように
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
の編集によって、ブラウザにページ表示する
代わりに、ダウンロードと保存するよう指示します。
Oct 26, 2017