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

Writing web applications can be monotonous, because we repeat certain patterns again and again. Django tries to take away some of that monotony at the model and template layers, but web developers also experience this boredom at the view level.

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

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

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

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

これらのビューを総合すると、開発者が遭遇する最も一般的なタスクを実行するためのインターフェースが提供されます。

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

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

これは、ジェネリックビューが1.3リリースで再設計された理由の一つです。以前は、ジェネリックビューはビュー関数でありながらも、多くのオプションが用意されていました。

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

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

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

TemplateView は確かに便利ですが、データベースコンテンツのビューを表示する場合、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

このテンプレートは、すべてのパブリッシャーオブジェクトを含むobject_listという変数を含むコンテキストに対してレンダリングされます。テンプレートは次のようになります。

{% extends "base.html" %}

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

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

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

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

まあ、モデルオブジェクトを扱っているのであれば、これは既に行われています。オブジェクトやクエリセットを扱う場合、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 つのクラスが同じキーを定義しようとしない場合、これは期待通りの結果をもたらします。しかし、親クラスがキーを設定した後に (スーパーを呼び出した後に) あるクラスがキーをオーバーライドしようとした場合、そのクラスの子クラスも親クラスを確実にオーバーライドしたいのであれば、スーパーの後に明示的にキーを設定する必要があります。問題がある場合は、ビューのメソッドの解決順序を見直してみてください。

もうひとつの考慮点は、クラスベースのジェネリックビューのコンテキストデータが コンテキストプロセッサによって提供されるデータを上書きしてしまうことです。たとえば、 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 オブジェクトの詳細については クエリを作成する を、詳細についてはクラスベースの class-based views reference を参照してください)。

例を挙げれば、出版日ごとに本のリストを並べ替えることができます。最新のものが最初になります:

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'

これはごくわずかな例ですが、アイデアをうまく​​示しています。通常、オブジェクトを並べ替えるだけでは不十分です。特定の出版社の本のリストを提示したい場合は、同じ手法を使用できます:

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 引数というものもあります。詳しくは 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 PublisherBookListView

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

Next, we'll write the PublisherBookListView view itself:

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

Using get_queryset to add logic to the queryset selection is as convenient as it is powerful. For instance, if we wanted, we could use self.request.user to filter using the current user, or other more complex logic.

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

# ...

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 をフィルターするのに使うプライマリキーの値を見付けるためのデフォルトの名前です。

If you want to call the group something else, you can set pk_url_kwarg on the view.