ビルトインのクラスベースのジェネリックビュー

ウェブアプリケーションを書くのは、特定のパターンを何度も繰り返すことになるため、単調になりがちです。Django はモデルとテンプレート層で単調さを取り除くことを試みてきましたが、ウェブ開発者はビューレベルでもこの種の退屈な繰り返しを経験してきました。

Django の ジェネリックビュー は、この苦痛を軽減するために開発されました。ビューの開発には共通のイディオムとパタンが存在するため、それらを抽象化することで、共通のビューデータを少ないコードで素早く記述することができます。

私たちはオブジェクトのリスト表示のような特定の共通タスクを認識することで、任意の オブジェクトのリストを表示するコードを書きます。そして、対象のモデルを URLconf から追加の引数として渡します。

Django のジェネリックビューを使うと、以下のことが可能になります。

  • オブジェクトのリストと、1つのオブジェクトに対する詳細ページの表示。カンファレンスを管理するアプリケーションを作っている場合、リストビューの例としては、TalkListViewRegisteredUserListView といったものが考えられます。1つのトークの情報を表示するページが、いわゆる「詳細」ビューの一例です。
  • 日付を基本とするオブジェクトと、年・月・日のアーカイブページ、関連する詳細ページと「最新」ページの表示。
  • ユーザーにオブジェクトの作成、更新、削除を可能にする (認証のあり・なしいずれでも)。

これらを組み合わせることで、開発者が遭遇する多くの共通のタスクを実行する簡単なインタフェースが提供されます。

ジェネリックビューを拡張する

言うまでもなく、ジェネリックビューは実質的に開発をスピードアップさせてくれます。しかし、多くのプロジェクトでは遅かれ早かれジェネリックビューだけでは十分ではなくなる瞬間が訪れます。実際、新しい Django 開発者から最もよく聞かれる質問は、幅広い状況に対処するためにジェネリックビューを拡張するにはどうすれば良いのか、というものです。

これが、ジェネリックビューが 1.3 のリリースで再設計された理由の一つです。それ以前は、おびただしい数のオプションを設定する必要がある単なるビュー関数でした。現在では、多数の設定を URLconf 内で渡すのではなく、ジェネリックビューのサブクラス化を作成し、属性とメソッドを上書きする方法が推奨されています。

上で述べたように、ジェネリックビューには限界があります。自作のビューをジェネリックビューのサブクラスとして実装することに四苦八苦していると、それよりも自作のクラスベースまたは関数ベースのビューを使った方が効率的なのではないかと思うかもしれません。

ジェネリックビューの例はサードパーティアプリケーションでも利用できます。あるいは、自分で必要に応じてアプリケーションを作ることもできます。

オブジェクトのジェネリックビュー

TemplateView はたしかに便利ですが、Django のジェネリックビューが本当の輝きを見せるのは、データベース内のコンテンツを表示するビューを作成する時です。これは非常に一般的なタスクなので、Django はオブジェクトのリストと詳細ビューを非常に簡単に生成するためのビルトインのジェネリックビューをいくつか用意しているのです。

オブジェクトのリストや個々のオブジェクトを表示する例から見てみましょう。

ここでは、以下のようなモデルを使用します。

# 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()

    class Meta:
        ordering = ["-name"]

    def __str__(self):
        return self.name

class Author(models.Model):
    salutation = models.CharField(max_length=10)
    name = models.CharField(max_length=200)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='author_headshots')

    def __str__(self):
        return self.name

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

次に、ビューを定義しましょう。

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher

最後に、このビューを URL にフックさせます。

# urls.py
from django.urls import path
from books.views import PublisherList

urlpatterns = [
    path('publishers/', PublisherList.as_view()),
]

書く必要のある Python コードは、これですべてです。まだテンプレートを書く必要はありますけれど。ビューで使用するテンプレート名を template_name 属性に明示的に書くこともできますが、この属性を省略した場合でも、Django はオブジェクト名から推論してくれます。この場合は、テンプレート名は "books/publisher_list.html" になります。"books" の部分はモデルを定義したアプリの名前から来ていますが、"publisher" の部分はモデル名を単純に小文字にした名前になっています。

注釈

したがって、たとえば TEMPLATES 内で DjangoTemplates バックエンドの APP_DIRS オプションを True に設定した場合、テンプレートの場所は次のパスになります。/path/to/project/books/templates/books/publisher_list.html

このテンプレートは、すべての publisher オブジェクトを含んだ object_list という変数を持つコンテキストとともにレンダリングされます。よって、非常に簡単なテンプレートは次のように書けます。

{% extends "base.html" %}

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

That's really all there is to it. All the cool features of generic views come from changing the attributes set on the generic view. The generic views reference documents all the generic views and their options in detail; the rest of this document will consider some of the common ways you might customize and extend generic views.

「親切な」テンプレートコンテキストを作る

コード例の出版社をリストするテンプレートが、すべての出版社を``object_list`` という名前の変数に格納していたことに気づいたかもしれません。これでもたしかに機能的には正しく動作しますが、テンプレートを書く人にとっては、とてもではありませんが「親切」とは言えません。ここではこの変数には出版社のリストが入っているのだと「事実として知らなければならない」わけです。

実は、モデルオブジェクトを扱っている場合には、この問題はすでに対処されています。オブジェクトまたは queryset を扱う時、Django はモデルのクラス名を小文字にした名前を使ってコンテキストを設定できます。デフォルトの object_list エントリーに加えて、たとえば publisher_list のような変数に、全く同じデータが格納されているのです。

しかし、この名前でも良くないと感じるなら、コンテキストの変数名を手動で設定することもできます。次のように、ジェネリックビューの context_object_name 属性を設定すると、コンテキスト変数の名前として使えるようになります。

# views.py
from django.views.generic import ListView
from books.models import Publisher

class PublisherList(ListView):
    model = Publisher
    context_object_name = 'my_favorite_publishers'

分かりやすい context_object_name を設定するのはいつでも良い考えです。テンプレートのデザイン担当の同僚に、きっと感謝されるでしょう。

追加のコンテキストを追加する

ジェネリックビューが提供する以外の追加情報が必要になることはよくあります。たとえば、各出版社の詳細ページで全書籍リストを表示したくなったとします。DetailView ジェネリックビューは出版社をコンテキストに設定してくれますが、テンプレートに必要な他の追加情報を使うにはどうすれば良いのでしょうか?

その答えは、DetailView をサブクラス化して、get_context_data メソッドを自分で実装することです。デフォルトの実装では、単にテンプレートで表示されるオブジェクトだけを追加しますが、ここで追加情報を送るようにオーバーライドすることができます。

from django.views.generic import DetailView
from books.models import Book, Publisher

class PublisherDetail(DetailView):

    model = Publisher

    def get_context_data(self, **kwargs):
        # Call the base implementation first to get a context
        context = super().get_context_data(**kwargs)
        # Add in a QuerySet of all the books
        context['book_list'] = Book.objects.all()
        return context

注釈

Generally, get_context_data will merge the context data of all parent classes with those of the current class. To preserve this behavior in your own classes where you want to alter the context, you should be sure to call get_context_data on the super class. When no two classes try to define the same key, this will give the expected results. However if any class attempts to override a key after parent classes have set it (after the call to super), any children of that class will also need to explicitly set it after super if they want to be sure to override all parents. If you're having trouble, review the method resolution order of your view.

Another consideration is that the context data from class-based generic views will override data provided by context processors; see get_context_data() for an example.

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

それでは次に、すべての場所で使っていた model 引数について詳しく見ていきましょう。model 引数は、ビューの操作対象となるデータベースのモデルを指定します。この引数は、1オブジェクトまたは複数オブジェクトを操作するすべてのジェネリックビューで使用可能です。しかし、model 引数は操作対象のオブジェクトを指定する唯一の方法ではありません。次のように、queryset 引数を使ってオブジェクトを指定することもできます。

from django.views.generic import DetailView
from books.models import Publisher

class PublisherDetail(DetailView):

    context_object_name = 'publisher'
    queryset = Publisher.objects.all()

model = Publisher と指定するのは、実際には queryset = Publisher.objects.all() のショートカットでしかありません。しかし、queryset を使ってフィルタリングされたオブジェクトのリストを定義すれば、特定のオブジェクトだけをビューに表示させることができます。QuerySet の詳細については クエリを作成する を、この機能の完全な詳細については class-based views reference を参照してください。

簡単な例として、本を出版日で新しい順に並び替えたくなったとしたら、次のように書けます。

from django.views.generic import ListView
from books.models import Book

class BookList(ListView):
    queryset = Book.objects.order_by('-publication_date')
    context_object_name = 'book_list'

これはごく単純な例ですが、queryset の概念は上手く表現できていると思います。もちろん、普通はただ並び替える以上のことをすることになるでしょう。特定の出版社の書籍リストを表示したい場合にも、同じテクニックが使えます。

from django.views.generic import ListView
from books.models import Book

class AcmeBookList(ListView):

    context_object_name = 'book_list'
    queryset = Book.objects.filter(publisher__name='ACME Publishing')
    template_name = 'books/acme_list.html'

queryset がフィルタリングされただけではなく、テンプレート名も変更されているのがわかります。そうしないと、ジェネリックビューは「素の」オブジェクトリストと同じテンプレートを使ってしまいます。しかし、それは意図したものではないはずです。

同時に注意しておきたいのは、この方法は特定の出版社の本をリストアップするにはあまりエレガントな方法ではないということです。新しく出版社のページを追加する必要が生じるたびにURLconf に数行を追加する必要があるので、これでは数社以上追加するとなるとすでに無理があると分かるでしょう。この問題の解決策は、次のセクションで議論します。

注釈

/books/acme のリクエスト時に 404 が表示された場合は、本当に 'ACME Publishing' という名前を持つ Publisher が存在しているか確認してください。このようなケースのためにジェネリックビューには allow_empty 引数というものもあります。詳しくは class-based-views reference をご覧ください。

動的なフィルタリング

もう一つのよくあるニーズは、リストページの URL に指定した何らかのキーを使って、表示するオブジェクトをフィルタリングすることです。上の例では、出版社名を URLconf にハードコーディングしてしまっていましたが、もし任意の出版社に対するすべての書籍を表示するようなビューを書きたい場合には、どうすればいいでしょうか?

簡単なことです。ListView には、オーバーライドできる get_queryset() メソッドが定義されています。今までは queryset 属性の値を返すだけでしたが、さらにここにロジックを追加することができます。

この機能がうまく動作するキーポイントは、クラスベースのビューが呼ばれる段階で self 内には様々な便利な値が格納されていることです。request (self.request)、位置引数 (self.args)、そして、キーワード引数 (self.kwargs) が、URLconf からキャプチャされてきています。

ここでは、次のように URLconf に1つのキャプチャグループがあるとしましょう。

# urls.py
from django.urls import path
from books.views import PublisherBookList

urlpatterns = [
    path('books/<publisher>/', PublisherBookList.as_view()),
]

次に、PublisherBookList ビュー本体を書きます。

# views.py
from django.shortcuts import get_object_or_404
from django.views.generic import ListView
from books.models import Book, Publisher

class PublisherBookList(ListView):

    template_name = 'books/books_by_publisher.html'

    def get_queryset(self):
        self.publisher = get_object_or_404(Publisher, name=self.kwargs['publisher'])
        return Book.objects.filter(publisher=self.publisher)

このように、queryset の選択に追加のロジックを加えることはいともたやすいことです。望むなら、self.request.user で現在のユーザーの情報でフィルタリングしたり、もっと複雑なロジックを追加することも可能です。

次のようにすれば、テンプレートで使えるように、出版社の情報を同時にコンテキストに追加することもできます。

# ...

def get_context_data(self, **kwargs):
    # Call the base implementation first to get a context
    context = super().get_context_data(**kwargs)
    # Add in the publisher
    context['publisher'] = self.publisher
    return context

追加の処理を実行する

最後に見る共通パタンは、ジェネリックビューの呼び出しの前後で追加の処理を実行するというものです。

Author モデルに last_accessed フィールドがあり、誰かが最後に著者の情報を見た時刻をトラッキングするのに使用しているとします。

# models.py
from django.db import models

class Author(models.Model):
    salutation = models.CharField(max_length=10)
    name = models.CharField(max_length=200)
    email = models.EmailField()
    headshot = models.ImageField(upload_to='author_headshots')
    last_accessed = models.DateTimeField()

もちろん DetailView クラスはこのフィールドの存在について何も知りません。しかし、繰り返しになりますが、フィールドの更新を維持するカスタムビューを書くのは簡単なことです。

まず、著者の詳細ビューを追加して、URLconf にカスタムビューを使うようにする必要があります。

from django.urls import path
from books.views import AuthorDetailView

urlpatterns = [
    #...
    path('authors/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'),
]

そして、新しいビューを書きます。ここで、get_object はオブジェクトを取得するのに使用するメソッドで、今回は単にオーバーライドして呼び出しをラップしています。

from django.utils import timezone
from django.views.generic import DetailView
from books.models import Author

class AuthorDetailView(DetailView):

    queryset = Author.objects.all()

    def get_object(self):
        # Call the superclass
        object = super().get_object()
        # Record the last accessed date
        object.last_accessed = timezone.now()
        object.save()
        # Return the object
        return object

注釈

ここで URLconf は pk という名前のキャプチャグループを使っています。この名前は DetailView が queryset をフィルターするのに使うプライマリキーの値を見付けるためのデフォルトの名前です。

グループ名を何か他の名前にしたい場合は、ビューで pk_url_kwarg を設定してください。より詳しくは、DetailView のリファレンスに書かれています。