組み込みのクラスベースのジェネリックビュー(汎用ビュー)

ウェブアプリケーションを書くのは、特定のパターンを何度も繰り返すことになるため、単調になりがちです。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 PublisherListView(ListView):
    model = Publisher

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

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

urlpatterns = [
    path("publishers/", PublisherListView.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 %}

ジェネリックビューの優れた機能はすべて、ジェネリックビューに設定された属性を変更することで得られます。この ジェネリックビューのリファレンス のドキュメントの残りの部分では、ジェネリックビューをカスタマイズしたり拡張したりする一般的な方法のいくつかを検討します。本当にそれだけです。

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

コード例の出版社をリストするテンプレートが、すべての出版社を 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 PublisherListView(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 PublisherDetailView(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

注釈

通常、 get_context_data はすべての親クラスのコンテキストデータを現在のクラスのコンテキストデータにマージします。コンテキストを変更したい自分のクラスでこの動作を維持するには、必ずスーパークラスで get_context_data を呼び出すようにしてください。2 つのクラスが同じキーを定義しようとしない場合、これは期待通りの結果をもたらします。しかし、親クラスがキーを設定した後に (super を呼び出した後に) あるクラスがキーをオーバーライドしようとした場合、そのクラスの子クラスも親クラスを確実にオーバーライドしたいのであれば、super の後に明示的にキーを設定する必要があります。問題がある場合は、ビューのメソッドの解決順序を見直してみてください。

もうひとつの考慮点は、クラスベースのジェネリックビューのコンテキストデータが コンテキストプロセッサによって提供されるデータを上書きしてしまうことです。たとえば、 get_context_data() を参照してください。

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

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

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


class PublisherDetailView(DetailView):
    context_object_name = "publisher"
    queryset = Publisher.objects.all()

model = Publisher を指定することは、queryset = Publisher.objects.all() と言うのと同じことです。しかし、queryset を使用してオブジェクトのフィルタリングされたリストを定義することで、ビューで表示されるオブジェクトについてより具体的にすることができます(QuerySet オブジェクトについての詳細は クエリを作成する を参照し、完全な詳細については クラスベースのビューのリファレンス を参照してください)。

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

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


class BookListView(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 AcmeBookListView(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 引数というものもあります。詳しくは クラスベースビューのリファレンス を参照してください。

動的なフィルタリング

もう一つのよくあるニーズは、リストページの 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 PublisherBookListView

urlpatterns = [
    path("books/<publisher>/", PublisherBookListView.as_view()),
]

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

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


class PublisherBookListView(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 の選択に追加のロジックを加えるために get_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):
        obj = super().get_object()
        # Record the last accessed date
        obj.last_accessed = timezone.now()
        obj.save()
        return obj

注釈

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

グループを別の名前で呼び出したい場合は、ビューに pk_url_kwarg を指定します。

Back to Top