【Django入門15】クラスベースビューの基本

クラスベースビューの解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django のビューの1つの種類である「クラスベースビュー」について解説していきます!

Contents

クラスベースビュー

まずは、クラスベースビューとは何なのか?この点について解説していきたいと思います。

クラスベースビューとは

クラスベースビューとは、ビューの種類の1つです。ビューは、Django の基本構成となる MTV における V となります。

そして、クラスベースビューとは、クラスの定義によって作成されるビューのことを言います。

その他のビューの種類としては「関数ベースビュー」が挙げられます。この Django 入門 の連載においては、下記ページでビューの解説を行いましたが、ここで紹介したビューは関数ベースビューとなります。また、Django 入門 の連載の中で開発してきている掲示板アプリのビューに関しても関数ベースビューとなります。

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

スポンサーリンク

クラスベースビューと関数ベースビューの違い

では、このクラスベースビューと関数ベースビューの違いとは何なのでしょうか?

ビューの作り方が異なる

この2つではビューの作り方、言い換えれば views.py に定義する対象が異なります。

関数ベースビューでは、views.py に関数を定義することでビューを作成していくことになります。それに対し、クラスベースビューの場合は、前述の通り views.py にクラスを定義することでビューを作成していくことになります。その名の通り、関数ベースビューとは関数のビューであり、クラスベースビューとはクラスのビューとなります。

もっと詳細に言えば、関数ベースビューの場合、views.py へ定義する関数に処理を実装していくことでビューを作成していきます。クラスベースビューの場合、views.py に Django フレームワークが提供するクラスのサブクラスを定義し、そのサブクラスのカスタマイズを行うことでビューを作成していきます。

関数ベースビューとクラスベースビューの作り方の違い

このカスタマイズは、クラス変数の定義メソッドのオーバーライドによって実現されます。オーバーライドを行う場合はクラスベースビューでも処理を実装することになりますが、それもサブクラスのカスタマイズの一種であると捉えていただければ良いです。

関数ベースビューの例

次は簡単な例を確認しながら、関数ベースビューとクラスベースビューの違いについて考えていきたいと思います。

例えば下記の views.py における comments_view は関数を定義することで作成されているため、この comments_view は「関数ベースビュー」であると考えられます。

関数ベースビューのviews.py
from django.shortcuts import render
from .models import Comment

def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)

この comments_view 関数は下記ページの 掲示板アプリでモデルを利用してみる で示した views.pycomments_view 関数と同じものになります。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

この comments_view 関数はリクエストを受け取り、Comment.objects.all() により Comment というモデルクラス(テーブル)から全インスタンス(レコード)を取得するクエリを生成し、それをテンプレートファイルから comments という名前で参照できるようにコンテキスト context にセットします。

さらに、comments_view 関数は引数にテンプレートファイル 'forum/comments.html' とコンテキスト context  を指定して render 関数を実行し、その実行結果を return するようになっています。これにより、render 関数でテンプレートファイルとコンテキストから HTML が生成され、その結果がレスポンスとして Django フレームワークに返却されることになります。あとは、そのレスポンスが Django フレームワークを経由してクライアントに返却され、このレスポンスの返却により、クライアント側でページの表示が行われることになります。

関数ベースビューにおけるcomments_viewの説明図

このサイトの Django 入門 の連載を読んでくださっている方であれば、上記はお馴染みの処理の流れになると思います。ここで重要なポイントは、前述の通り、上記の views.py のような関数ベースのビューにおいては実現したい処理の流れを実装する必要があるという点になります。

クラスベースビューの例

さて、先ほど示した comments_view は関数ベースビューとなります。それに対し、comments_view をクラスベースビューに置き換えた場合の views.py は下記のようになります。この views.py における CommentList は、先ほど示した関数ベースビューの comments_view と同じページの表示を実現するクラスとなります。

クラスベースビューのviews.py
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'

ご覧の通り、関数ベースビューの views.py とは全く作りが異なります。クラス変数の定義を行なっているだけで、処理などは記述されていません。ですが、この CommentList では、前述で示した comments_view と同じページの表示を実現することが可能です。なぜ、そのようなことが可能なのでしょうか?

答えは非常にシンプルで、上記の CommentList が継承している ListView が、comments_view と同じような処理を行うように実装されているからです。ListView は Django フレームワークで定義されている Viewのサブクラス となります。この ListView は get メソッドを備えており、その get メソッドが実行された時に、comments_view と同じような処理が行われるようになっています。この処理の流れを図で示したものが下図となります。

ListViewの処理の流れの説明図

MEMO

誤解を与えると良くないので補足しておきますが、この図は大雑把に ListView における get メソッドの処理の流れを表したものであり、細かく言えば、このメソッドの処理の流れはもっと複雑です

例えば、④で示した処理は ListView における get メソッドが終了した後に実行されるようになっています

が、まずは大雑把に上記のような処理の流れを捉えていただいた方が、関数ベースビューとクラスベースビュの違いや関係性について理解しやすいと思います

この図の青字で示した modelcontext_object_nametemplate_nameListView のクラス変数となります。そして、ListView を継承したクラスから、これらのクラス変数の定義を上書きすることができるようになっています。

また、ListView を継承することで、先ほど処理の流れを示した get メソッドも CommentList に継承されることになります。そして、この get メソッドは各種クラス変数の定義に従って動作するようになっています。そのため、クラス変数の定義を変更してやれば、それに伴って get メソッドの処理・動作が変化することになります。

例えば、model = Comment と定義しておけば、②では Comment.objects.all() が実行されることになります。model = User と定義しておけば、②では User.objects.all() が実行されることになります。

したがって、下記のように ListView を継承する CommentList クラスを定義すれば、

クラスベースビューのviews.py
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'

CommentList クラスでは、modelCommentcontext_object_name'comments'template_name'forum/comments.html' として扱われるようになるため、この CommentListget メソッドの処理の流れは下図のようになります。

CommentListの処理の流れの説明図

そして、この処理の流れは comments_view と同様のものであることが確認できると思います。つまり、ListView には comments_view で行っていたものと同様の処理が既に定義されており、その処理をクラス変数によってカスタマイズできるようになっています。上記の CommentList は、クラス変数を変更することで comments_view と同様の処理を実現するためのカスタマイズ例の1つとなります。

是非ここで覚えておいていただきたいのが、Django フレームワークには関数ベースビューで実装される典型的な処理の流れを実現するためのクラスが既に数多く用意されているという点になります。これらは全て View のサブクラスとして定義されており、その例の1つが ListView となります。実際には、これらのクラスは単純に View を継承しているだけでなく、様々なクラスを継承して実現されているのですが、最終的には View を継承しています。そして、こういった View を継承しているクラスのことを、このサイトでは Viewのサブクラス と呼ばせていただきます。

Viewのサブクラスの説明図

例えば、特定のモデルクラス のインスタンスを全て取得し、それらを 特定のテンプレートファイル に埋め込んで表示するような処理は “関数ベースビューで実装される典型的な処理の流れ” と言ってよいでしょう。皆さんもそんなビューを実装したことがあるのではないでしょうか?

まさに、その例の1つが comments_view となります。

そして、このような典型的な処理の流れは Django フレームワークで ListView によって既に実装されています。そして、この ListView のサブクラスを定義してカスタマイズしてやることで、特定のモデルクラス の部分や 特定のテンプレートファイル の部分を好きなように変更したビューを実現することができます。

Viewのサブクラスを継承するクラスでクラス変数の定義を行う様子

“関数ベースビューで実装される典型的な処理の流れ” は他にもあって、例えば 特定のモデルクラス における 特定の1つのインスタンス の情報を 特定のテンプレートファイル に埋め込んで表示するような処理もそれに当てはまると思います。このような処理の流れは Viewのサブクラス の1つである DetailView で実装されています。他にも、こういった様々な典型的な処理の流れを実現するための Viewのサブクラス が Django フレームワークには用意されています。それらの紹介は、後述の Viewのサブクラス の種類 で行います。

まずは、そういった Viewのサブクラス を継承するクラスの定義とカスタマイズによって様々なビューを実現することが可能となるという点、さらに、Viewのサブクラス を継承するクラスの定義とカスタマイズというやり方で作成していくビューがクラスベースビューであるという点については覚えておきましょう!

クラスベースビューの作り方

続いて、クラスベースビューを作成する手順について解説していきます。

Viewのサブクラス を継承するクラスを定義する

クラスベースビューを作る上で最初に行うことは、Django フレームワークに用意された Viewのサブクラス を継承するクラスの views.py への定義となります。

前述で “関数ベースビューで実装される典型的な処理の流れを実現するためのクラスが既に数多く用意されている” と説明しました。これらのクラスは全て、Django フレームワークで Viewのサブクラス として定義されています。

これらの多くは django.views.generic で定義されていますので(例外もあります)、django.views.generic から実現したいビューに応じたサブクラスを import し、そのクラスを継承するサブクラスを定義します。

クラスの定義
from django.views.generic import Viewのサブクラス

class クラス名(Viewのサブクラス):
    pass

Viewのサブクラス の代表例の1つは前述でも使用例を示した ListView となります。ListView のサブクラスを CommentList として定義する場合は、まずは下記のように views.py を実装することになります。

クラスの定義例
from django.views.generic import ListView

class CommentList(ListView):
    pass

views.py には、実現したいビューの数だけ上記のようなクラスを定義することになります。その際には、実現したいことに応じて継承する Viewのサブクラス を適切に選択する必要があります。先ほども言いましたが、Viewのサブクラス の種類の詳細については Viewのサブクラス の種類 で別途紹介します。

スポンサーリンク

urls.py を作成する

関数ベースビューと同様に、クラスベースビューのビューを動作させるためには urls.py を作成する必要があります。ただし、クラスベースビューの場合と関数ベースビューの場合とでは urls.py (アプリ側) の作り方が若干異なります。

関数ベースビューの場合、urls.py では下記のように urlpatterns を定義し、path の第2引数には、第1引数の URL (URL パターン) へのリクエストをウェブアプリが受け取った際に実行させたい関数オブジェクトを指定する必要がありました。

関数ベースビューの場合のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path(URL, views.関数名),
    略
]

クラスベースビューの場合も基本的な形式は関数ベースビューの時と同様となるのですが、path の第2引数には、第1引数の URL (URL パターン) へのリクエストをウェブアプリが受け取った際に動作させたいクラスの as_view メソッドの実行結果を指定する必要があります。つまり、views.クラス名.as_view() を指定する必要があります。

クラスベースビューの場合のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path(URL, views.クラス名.as_view()),
    略
]

このような path への引数指定によって、第1引数に指定した URL と第2引数に指定した views.クラス名.as_view()クラス名 のクラスとがマッピングされることになります。

関数ベースビューの時と同じノリで views.クラス名 のみを指定しても上手くビューが動作しないので注意してください。また、クラスベースビューと関数ベースビューとの urls.py の作り方の違いはこの点のみで、クラスベースビューの場合でも path 関数に name 引数等を指定することが可能です。

このように URL とクラスとをマッピングしておけば、その URL のリクエストをウェブアプリが受け取った際に、その URL にマッピングされたクラスの持つ dispatch メソッドが実行されるようになります。さらに、その dispatch メソッドから、リクエストのメソッドに応じたクラスのメソッド(リクエストのメソッド名を小文字にしたメソッド)が実行されることになります。ちょっとメソッドという言葉が多くて分かりにくいですが、リクエストのメソッドが GET であれば get メソッドが実行されることになり、リクエストのメソッドが POST であれば、post メソッドが実行されることになります。

リクエストのメソッドに応じて実行されるメソッドが切り替えられる様子

そして、これらの get メソッドや post メソッドの中で、今まで私たちが関数ベースビューで実装してきたような処理が実行されることになります。クラスベースビューの例ListView を継承するクラスの get メソッドの処理の流れについて説明しましたが、それと同様に、各クラスには get メソッドや post メソッドが用意されており、それらがリクエストのメソッドに応じて自動的に実行されることになります。

そのため、下記ページの メソッドに応じた機能を実行する で解説したように、関数ベースビューではビューに定義した関数の中でリクエストのメソッドに応じた条件分岐を行う必要がありましたが、クラスベースビューの場合は、そういったメソッドに応じた条件分岐も不要となります。

Djangoのビューの機能についての解説ページアイキャッチ 【Django入門3】ビュー(View)の基本とURLのマッピング

ただし、Viewのサブクラス の種類によっては get メソッドと post メソッドの片方しか定義されていない場合があります。例えば、クラスベースビューの例 で例に挙げた ListView に関しては post メソッドが定義されていないクラスとなります。そのため、ListView がメソッドが POST のリクエストを受け取った場合はエラーのレスポンスが返却されることになります。それに対し、後述で紹介する CreateView に関しては get メソッドと post メソッドの両方が定義されており、メソッドが GET のリクエストも POST のリクエストも対応可能となっています。

このように、Viewのサブクラス の種類によって get メソッドと post メソッドの定義の有無が異なり、定義されていないメソッドに対するリクエストを受け取った場合はエラーのレスポンスが返却されることになるので、この点については是非覚えておいてください。

クラス変数を定義する

上記のように urls.py を作成したら、とりあえずクラスベースビューのビューが動作するための準備が整ったことになります。あとは、views.py に定義したクラスをカスタマイズしていけば良いだけです。

このカスタマイズは、大きく分けて「クラス変数の定義」と「メソッドのオーバーライド」によって実施していくことになります。まずは、前者のクラス変数の定義について解説していきます。

クラス変数の定義

このカスタマイズは、通常のクラス変数を定義するときと同様に、下記のように クラス変数名 = 値 といった感じで定義を行うことで実現していくことになります。

クラス変数の定義
from django.views.generic import Viewのサブクラス

class クラス名(Viewのサブクラス):
    クラス変数名1 = 値
    クラス変数名2 = 値

Viewのサブクラス で定義されるクラス変数を上書き

このクラス変数の定義を行う上で重要となる点は2つあって、1つ目が Viewのサブクラス で定義されているクラス変数を定義する必要があるという点になります。

このクラス変数の定義は、Viewのサブクラス の動作を設定するために行います。もう少し具体的に言うと、Viewのサブクラス で定義されているメソッドの動作を変化させることを目的に行います。

各種 Viewのサブクラス にはメソッドが定義されており、それらのメソッドの中で、Viewのサブクラス で定義されているクラス変数が参照されるようになっています。なので、Viewのサブクラス で定義されるクラス変数を「Viewのサブクラス を継承するクラス」で定義して上書きしてやれば、メソッドが参照するクラス変数の値が上書きされたものになるので、各種 Viewのサブクラス のメソッドの処理・動作が変化することになります。

Viewのサブクラスのメソッドが、そのサブクラスを継承するクラスのクラス変数を参照して処理を実行する様子

クラスベースビューの例 でも、ListView のサブクラスでクラス変数 model を Comment として定義することで ListViewget メソッドでのレコードの取得先のテーブルが Comment に設定される例を示しましたが、クラス変数 model を別のモデルクラスとして定義すれば、それに伴って ListViewget メソッドでのレコードの取得先のテーブルも変化することになります。

クラス変数modelの定義によってインスタンス取得先のテーブルが変化する様子

このように動作が変化するのは、ListViewget メソッドで、レコードの取得先のテーブルとしてクラス変数 model が参照されるようになっているためです。これと同様に、Viewのサブクラス の各種メソッドからは、Viewのサブクラス のクラス変数が参照されるようになっています。なので、このクラス変数を上書きすることで、Viewのサブクラス のメソッドの動作を変化させることが可能です。

そして、このクラス変数の定義の主な目的は「Viewのサブクラス で定義されているメソッドの動作を変化させること」ですので、メソッドから参照されていないクラス変数、つまり Viewのサブクラス で定義されていないクラス変数を定義してもあまり意味がありません。

例えば ListView では下記のようなクラス変数が定義されています。したがって、ListView のサブクラスに下記のクラス変数を定義することで、ListView の処理(get メソッド)の動作をカスタマイズすることができます。

  • model:レコードの取得先のテーブル(モデルクラス)を指定
  • queryset:レコードを取得するために発行するクエリを指定
  • ordering:取得するレコードの並びを指定
  • context_object_name:取得したレコードをセットする context のキーを指定(テンプレートファイルから取得したレコードを参照する変数名を指定)
  • paginate_by*:1ページに割り付けするレコードの数を指定
  • paginate_orphans*:最後のページに表示される中途半端な数のレコードを前のページに含めるかどうかを指定
  • page_kwarg*:ページ番号を示すクエリパラメータの変数名を指定
  • allow_empty:取得したレコードが 0 であることを許可するかどうかを指定
  • content_type:レスポンスボディのコンテンツタイプを指定
  • template_name:HTML 生成時に使用するテンプレートファイルを指定
  • template_name_suffixtemplate_nameが指定されなかった場合の、HTML 生成時に使用されるテンプレートファイルのファイル名のサフィックス

* を付けたクラス変数はページネーションに関わる設定になります。ページネーションとは多数のレコードを複数のページに割り付けて表示する機能であり、別途下記ページで解説をしていますので、詳しく知りたい方は下記ページをご参照ください。

Djangoにおけるページネーションの解説ページアイキャッチ 【Django入門13】ページネーションの基本

前述の通り、これらのクラス変数を ListView のサブクラスに定義することで、ListViewget メソッドの動作を変化させることが可能となります。ですが、上記のクラス変数を定義しても意味のない Viewのサブクラス も存在します。

例えば Viewのサブクラス には DetailView という1つのレコードの詳細情報を表示するクラスが存在します。DetailView は1つのレコードしか扱わないため、ページネーション機能は意味のないものとなります(ページネーション機能は複数のレコードを各ページに割り付ける機能です)。したがって、DetailView には上記で示した paginate_by などのページネーションに関わるクラス変数は定義されておらず、これらを DetailView のサブクラスに定義しても DetailView のサブクラスの動作は変化しないことになります。

Viewのサブクラスで定義されていないクラス変数を定義してもViewのサブクラスのメソッドからは参照されないことを示す図

このように、カスタマイズを行うためにクラス変数を定義する場合、Viewのサブクラス で定義されているクラス変数を上書きしてメソッドの動作を変化させることが目的となりますので、”Viewのサブクラス で定義されているクラス変数” を定義しないと意味がありません。そして、この定義されているクラス変数は Viewのサブクラス によって異なるので注意が必要です。各 Viewのサブクラス で定義される具体的なクラス変数の種類については、Viewのサブクラス の種類 で紹介するリンク先の各ページで紹介していますので、このページを読み終えた後にでも興味のある Viewのサブクラス のクラス変数について調べてみていただければと思います。

クラス変数のデフォルト設定

2つ目は、Viewのサブクラス を継承するクラスを定義したとしても、そのクラスでクラス変数を定義しない限り、そのクラスは Viewのサブクラス でのクラス変数の定義に従って動作するという点になります。もっと簡単に言えば、Viewのサブクラス を継承するクラスで定義していないクラス変数はデフォルト設定のままとなります。そして、そのデフォルト設定は、Viewのサブクラス で決められています。

例えば ListView における template_name のデフォルト設定は下記のように指定されています。

  • template_name'アプリ名/モデル名_list.html'

ここで モデル名 とはクラス変数 model に指定したモデルクラスの名前を全て小文字にしたものとなります。アプリ名 はアプリの名前で、これも全て小文字となります。

MEMO

もっと正確に言えば、この モデル名 はクラス変数 model に指定したモデルクラスの _meta 属性で指定される名称であり、これを変更している場合は上記のようなデフォルト設定にならないので注意してください

また、クラス変数として model ではなく query_set を定義している場合、その queryset からモデルクラスが特定され、そのモデルクラスの _meta 属性から上記のデフォルト設定が決定されることになります

このように、Viewのサブクラス 側でデフォルト設定が指定されているため、クラスベースビューを採用するアプリの開発方針としては2つのパターンが存在することになります。

1つ目は、用意するテンプレートファイルやコンテキストに合わせてクラス変数を定義するパターンで、2つ目は、クラス変数を定義せずにデフォルト設定に合わせたテンプレートファイルやコンテキストを用意するパターンとなります。

前者に関しては、ビュー以外に合わせてビューを作る考え方になりますし、後者に関してはビューに合わせてビュー以外を作る考え方となります。

ビューと、ビュー以外の部分の作り方の考え方

先ほどの例で言えば、用意したテンプレートファイルのパスに合わせてクラス変数 template_name を定義しても良いですし、template_name のデフォルト設定のパスにテンプレートファイルを用意するのでも良いです。要は、ビューとビュー以外の部分で話があっていれば良いです。この例の場合は、ビューが利用しようとしているテンプレートファイルのパスと実際に用意されているテンプレートファイルのパスが一致していれば良いです。

ですが、どちらかというと後者の方が良いと思います。なぜなら、それによりビュー以外の部分の作りに自然と一貫性が生まれるからです。さらに、それにより、各種ファイルの役割等が分かりやすくなります。

例えば、クラスベースビューの例 で例に挙げた CommentList の場合、modelComment を指定しているため、クラス変数 template_name を定義せずにビューを上手く動作させようとすると、自然とテンプレートファイルの名前が 'アプリ名/comment_list.html' に決まることになります。そして、ある程度 Django に詳しい人であれば、このテンプレートファイルの名前から、そのテンプレートファイルの役割をすぐに理解することができます。この場合は Comment というモデルクラスのインスタンスの一覧を表示するためのテンプレートであることが、 template_name のデフォルト設定の仕組みからすぐに分かります。

ということで、どちらかというとクラス変数を定義せずにデフォルト設定に応じたテンプレートファイルやコンテキストを用意するのが良いかと思います。後述の 掲示板アプリのビューをクラスベースに変更する では、既に以前の連載の中でテンプレートファイルなどを用意しているため、用意済みのテンプレートファイルに合わせるようクラス変数の定義を行う例を示しますが、ビューとビュー以外の部分の開発の方針としては上記のように2つのパターンがあって、後者の方がビュー以外の部分の統一感が出てアプリの構成が分かりやすくなることは是非覚えておいてください。

ただし、そもそも定義が必須となるクラス変数も存在するので注意してください。例えば ListView の場合は model or queryset のどちらかのクラス変数の定義が必須となります。

メソッドをオーバーライドする

また、クラス変数の定義だけでなく、メソッドをオーバーライドすることでビューのクラスをカスタマイズしていくことも可能です。

Viewのサブクラス には様々なメソッドが用意されています。そして、Viewのサブクラス が行う処理は、これらのメソッドを実行することで実現されています。したがって、これらのメソッドを Viewのサブクラス を継承するクラスでオーバーライドすることで、その処理の動作をカスタマイズすることが可能となります。

メソッドのオーバーライド

このカスタマイズに関しても通常のオーバーライド時と同様の手順で実現することができます。具体的には、サブクラス側にスーパークラス(Viewのサブクラス)の持つメソッドと同じ名前・引数のメソッドを定義してやれば良いだけです。これにより、その名前のメソッドが実行される際にスーパークラス側ではなくサブクラス側のメソッドが呼ばれるようになり、これによって処理の動作をカスタマイズすることができることになります。

メソッドのオーバーライド
from django.views.generic import Viewのサブクラス

class クラス名(Viewのサブクラス):
    クラス変数名1 = 値
    クラス変数名2 = 値

    def メソッド名1(self, 引数1, 引数2, ...)
        略

    def メソッド名2(self, 引数1, 引数2, ...)
        略

Viewのサブクラス で定義されるメソッドを上書き

これは、オーバーライドという言葉を使っているので当たり前になりますが、カスタマイズを目的に定義するメソッドは、継承する Viewのサブクラス で定義されているメソッドである必要があります。

この定義されているメソッドも Viewのサブクラス によって異なるため、各種 Viewのサブクラス で定義されるメソッドに関しても Viewのサブクラス の種類 で紹介するリンク先の各ページで紹介を行いたいと思います。

動的に設定を変化させるためにオーバーライドを利用する

このオーバーライドでカスタマイズを行うメリットは、そのクラスの設定を動的に変化させることができるという点にあります。

例えば、クラスベースビューの例 で紹介した CommentList の例で考えると、ListView には get_queryset というメソッドが定義されているため、この get_querysetCommentList で定義してオーバーライドしてやれば、CommentList での get_queryset 実行時の動作を継承元の ListView とは異なるものにすることができます。

get_queryset
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'

    def get_queryset(self):
        return Comment.objects.all().order_by('-date')

get_querysetListView によって定義される get メソッドから呼び出しされるメソッドで、データベースに発行するクエリを取得するメソッドとなります。この発行するクエリによって、データベースから取得できるインスタンスが変わることになります。そして、この ListViewget メソッドは CommentList に継承されることになり、CommentList のインスタンスから get メソッドが実行された際には、上記でオーバーライドを行なった CommentListget_queryset が呼び出しされることになります。

そのため、上記のように get_queryset をオーバーライドすることでデータベースに発行するクエリを ListView のものから変化させることができ、CommentList 用にカスタマイズすることが可能となります。上記の場合は、データベースから取得するインスタンスが date フィールドに対して降順にソートされることになります。

ですが、実は上記のように get_queryset をオーバーライドすることはあまり意味がありません。なぜなら、get_queryset が返却する値は毎回同じだからです。毎回 “Comment のテーブルレコードを全て取得して date フィールドに対して降順にソートする” というクエリが返却されるだけです。つまり、この get_queryset が返却する値は静的に決まるということになります。

こういった静的に決まる設定に関しては、メソッドのオーバーライドではなくクラス変数の定義で行うことが可能であるケースが多いです。例えば上記の例であれば、わざわざメソッドのオーバーライドを行わなくても下記のように ordering というクラス変数を定義することで実現可能です(クラス変数 queryset の定義によっても実現可能です)。

orderingの定義でソートを行う例
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'
    template_name = 'forum/comments.html'
    ordering = '-date'

クラス変数の定義でもメソッドのオーバーライドによってもカスタマイズ可能なものに関しては、どちらかというとクラス変数の定義で実現する方が良いと思います。なぜなら、処理を記述するとバグが発生する可能性が高くなるからです。メソッドを定義すると、必ず処理の記述が必要となります。ですので、特に静的に決まる設定等のカスタマイズを行う場合はクラス変数の定義で行った方が無難だと思います。

もう少し詳細を説明すると、これらのクラス変数はアプリ起動時に Python によって解釈されることになります。したがって、アプリ起動時に既に決まっている設定等のカスタマイズはクラス変数の定義によって実現することができ、上記の通り、その方が無難です。

ですが、アプリ起動時には設定等が決まらない場合もあります。その場合、クラス変数の定義によるカスタマイズは不可となります。それに対し、メソッドのオーバーライドの場合、アプリ起動時ではなくメソッドが実行されたタイミングで設定を動的に変化させることが可能となります。したがって、アプリ起動時には設定等が決まらないカスタマイズ、言い換えれば、設定等を動的に変化させる必要のあるカスタマイズに関しては、メソッドのオーバーライドで実現する必要があります。

例えば、ユーザー登録後に、自動的にその登録されたユーザーの詳細ページに遷移させる処理について考えてみましょう!このユーザーの詳細ページの URL パターンは下記のようなものであるとしたいと思います。username は登録されたユーザーのユーザー名とします。そして、ここで実現しようとしているのは、ユーザー登録後の下記 URL へのリダイレクトとなります。

user/<slug:username>/

こういったユーザーの登録はレコードの新規登録と考えられ、このレコードの新規登録を行うのに便利な ViewのサブクラスCreateView となります。この CreateView では、ユーザー登録フォームから送信されてきたデータの妥当性の検証を行い、検証結果が OK の場合、そのデータをレコードとしてデータベースに保存し、さらに success_url というクラス変数によって指定される URL にリダイレクトするようになっています(”リダイレクトする” とは、ウェブアプリ側からすれば “リダイレクトレスポンスを返却すること” となります)。

success_urlへのリダイレクトを行う様子

ですので、CreateView のサブクラス側で success_url をクラス変数として定義してやれば、ユーザー登録後に、success_url  の定義に応じた URL にリダイレクトすることができるようになります。

ですが、先ほど示した URL において、username の部分は登録されたユーザーの username (ユーザー名) によって変化することになります。ユーザーが設定する username は、実際にユーザー登録された後のタイミングでしか分かりません。したがって、アプリ起動時にリダイレクト先の URL を決定することは不可能です。つまり、こういったリダイレクトはクラス変数の定義では実現できません。

こんな時に利用するのがメソッドのオーバーライドによるカスタマイズになります。アプリ起動時にリダイレクト先の URL を設定することはできませんが、ユーザー登録後であれば、登録されたユーザーの username は既に分かっているため、リダイレクト先の URL を username に応じたものに設定することは可能です。

そして、CreateView では下記の get_success_url メソッドが定義されています。この get_success_url は、レコード新規登録後のリダイレクト先の URL を返却するメソッドで、さらに、レコードの新規登録後に実行されるメソッドです。

get_success_url
def get_success_url(self):
    """Return the URL to redirect to after processing a valid form."""
    if self.success_url:
        url = self.success_url.format(**self.object.__dict__)
    else:
        略
    return url

なので、CreateView のサブクラスで get_success_url をオーバーライドし、登録されたレコードの username に応じた URL 返却するようにすれば、登録されたユーザーの詳細ページへのリダイレクトが実現できることになります。その具体例が下記となります。

urlの動的な指定
def get_success_url(self):
    return reverse('user', kwargs={'username':self.object.username})

この get_success_url に関しては CreateView の解説ページで詳しく説明しますが、ここで理解しておいていただきたいのは、動的にパラメーターを変化させたい場合はクラス変数の定義ではなくメソッドのオーバーライドによって実現する必要があるという点になります。クラス変数の定義でもメソッドのオーバーライドでも、どちらでもカスタマイズ可能なのであれば、前述でも説明したようにクラス変数の定義で実現するのが良いと思います。

まずは、ここで説明したことを基準に、クラス変数の定義 or メソッドのオーバーライドのどちらでカスタマイズするのかを決めるので良いと思います。ただし、そもそもクラス変数の定義ではカスタマイズ不可の場合もあるため、その場合はメソッドのオーラーライドによってカスタマイズを行う必要があります。メソッドのオーバーライドによるカスタマイズの方がカスタマイズ可能な範囲が広いです。

スポンサーリンク

Mixin で機能を追加する

ここまでの説明のとおり、Viewのサブクラス を継承するクラスでのクラス変数の定義メソッドのオーバーライドによって、ウェブアプリ特有のビューを実現していくというのが、クラスベースビューの作り方の基本的な考え方になります。

ただ、これらのクラス変数の定義やメソッドのオーバーライドだけでなく、Mixin を利用してクラスベースビューに機能を追加する方法もあるので、これについても簡単に説明しておきます。

例えば、下記ページで解説しているログイン機能をクラスベースビューで実現する際には、Mixin を利用した機能追加が必要となります。

ログインの実現方法の解説ページアイキャッチ 【Django入門10】ログイン機能の実現

上記ページでも解説しているように、このログイン機能を実現する際には、単にログインを実現するだけでなく、非ログインユーザーからのアクセスを禁止することも必要となります。そしてこれは、関数ベースビューの場合、@login_required を関数に指定することで実現可能です。

具体的には、下記のように comments_view に @login_required を指定すれば、非ログインユーザーからのリクエストによって comments_view が実行されようとする際に、強制的に他のページ(例えばログインページ)にリダイレクトされるようになります。したがって、非ログインユーザーは comments_view の実行によって表示されるページにはアクセスできないことになります。

関数への@login_requiredの指定
@login_required
def comments_view(request):
    # 略

ただし、この @login_required は関数に指定可能なものであって、クラスには指定不可です。そのため、クラスベースビューの場合は @login_required の利用は不可ということになります。したがって、クラスベースビューの場合は別の手段で非ログインユーザーからのページのアクセスを防ぐ必要があります。

この例で言えば、その別の手段とは “LoginRequiredMixin を継承する” になります。例えば下記のように LoginRequiredMixin を継承させれば、CommentList はログイン中のユーザーからのリクエスト時のみ実行されるようになり、それ以外のユーザーからのリクエストの場合は別のページへリダイレクトされることになります。つまり、CommentList によって表示されるページはログイン中のユーザーからしかアクセスできないことになります。

LoginRequiredMixin
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin

class CommentList(LoginRequiredMixin, ListView):
    pass

ちょっと限定的な例での説明になってしまいましたが、このように、Mixin を継承することでクラスのカスタマイズを行うことができる点も是非覚えておいてください。

Viewのサブクラス の種類

続いて、Django フレームワークに用意された Viewのサブクラス を紹介していきます。前述の通り、クラスベースビューは Viewのサブクラス を継承してカスタマイズしていくことで作成していくことになります。ただし、Django フレームワークに用意されている Viewのサブクラス には様々なものが存在しており、これらの中から実現したいビューに合わせて適切な Viewのサブクラス を選択して継承する必要があります。

そのため、各種 Viewのサブクラス の特徴はしっかり覚えておいた方が良いと思います。

ここでは、Viewのサブクラス の種類とともに、これらの特徴について簡単に説明していきます。各 Viewのサブクラス の詳細についてはリンク先のページで別途詳細を解説していますので、必要に応じてこれらのページも参照していただければと思います。

ListView

まず最初に紹介するのが ListView になります。ここまでの説明の中でも何回か登場しましたが、ListView は特定のモデルクラス(テーブル)のインスタンス(レコード)の一覧を表示するビューを作る時に継承する Viewのサブクラス となります。

例えばコメント一覧、ユーザー一覧などを表示するビューは ListView を使えば簡単に実現可能です。

ListViewで実現できるページの例

この ListView の詳細に関しては下記ページで解説を行なっています。

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

スポンサーリンク

DetailView

ListView が一覧を表示する際に継承されるのに対し、DetailView は特定のモデルクラスの1つのインスタンスの詳細を表示する際に継承される Viewのサブクラス となります。

例えば、特定のユーザーの詳細を表示するビューなどは DetailView を継承することによって簡単に実現することができます。

DetailViewで実現できるページの例

この DetailView の詳細に関しては下記ページで解説を行なっています。

DjangoのDetailViewの解説ページアイキャッチ 【Django】DetailViewの使い方(クラスベースビューでの詳細ページの実現)

CreateView

ListViewDetailView がデータベースからレコードを取得して情報を表示する目的で継承されるのに対し、ここから紹介するクラスはデータベースのテーブルやレコードを変更する目的で継承される Viewのサブクラス となります。ここでいう変更とは、新規登録・更新・削除のことを言っています。

まず紹介するのが CreateView で、CreateView は特定のモデルクラスのインスタンスをレコードとしてデータベースに新規登録・新規作成することを目的に継承される Viewのサブクラス となります。

レコードの新規登録はレコードを保存するテーブルの種類によって様々な意味合いとなり、例えばユーザーの新規登録やコメントの新規投稿などを行うこともレコードの新規登録と捉えることができます。そして、これらは CreateView を継承することで簡単に実現することが可能です。

CreateViewで実現できるページの例

ListViewDetailView とは異なり、この CreateView ではフォームを扱うことが可能です(以降で紹介する Viewのサブクラス も同様です)。関数ベースビューの場合、フォームを扱う際にはリクエストのメソッドが GET の場合と POST の場合とで処理が切り替えられるように実装する必要がありました。正直それが面倒だったのですが、CreateView 等のフォームを扱う Viewのサブクラス を継承してビューを作成する場合は、Viewのサブクラス 内部でリクエストのメソッドに応じた処理の切り替えが行われるように作られているため、そういった処理の記述は不要となって楽にフォームを扱うことができるようになります。

CreateViewの中でメソッドに応じた処理の切り替えが行われるようになっている様子

この CreateView の詳細に関しては下記ページで解説を行なっています。

CreateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】CreateViewの使い方(クラスベースビューでの新規登録ページの実現)

UpdateView

CreateView がレコードの新規登録を行う Viewのサブクラス であるのに対し、UpdaateView はレコードの更新を行う Viewのサブクラス となります。

例えばユーザーの情報の更新ページのビューなどは UpdateView を継承することで簡単に実現することができます。

UpdateViewで実現できるページの例

この UpdateView の詳細に関しては下記ページで解説を行なっています。

UpdateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】UpdateViewの使い方(クラスベースビューでのレコード更新ページの実現)

スポンサーリンク

DeleteView

DeleteView はレコードの削除を行う Viewのサブクラス となります。

例えば投稿済みのコメントを削除したり、ユーザーの退会を行うビューなどは、DeleteView を継承することで簡単に実現することができます。

DeleteViewで実現できるページの例

この DeleteView の詳細に関しては下記ページで解説を行なっています。

DeleteViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】DeleteViewの使い方(クラスベースビューでのレコード削除ページの実現)

FormView

FormView はフォームを扱う Viewのサブクラス となります。

ここまで紹介してきた CreateViewUpdateViewDeleteView でもフォームを扱うことができますが、これらとの違いは、FormView は基本的にデータベースのレコードの変更は行わないという点になります。もう少し違う言い方をすれば、前述の3つに関してはデータベースのレコードの変更を行うことを前提としたクラスであるのに対し、FormView ではその前提はありません。単にフォームを扱うことに特化した Viewのサブクラス となります。

例えば CreateView では post メソッドが定義されており、この post メソッドではフォームから送信されてきたデータをレコードとしてデータベースに新規登録する処理が行われるようになっています。それに対し、FormView でも post メソッドが定義されていますが、この post ではデータベースの操作は行われません。

したがって、基本的には FormView はデータベースのレコードの変更を伴わないようなフォームを扱う際に継承することになります。

ただし、FormView を継承するクラスで post メソッドをオーバーライドすることでデータベースへの操作を行うようなことも可能です。つまり、FormView は CreateViewUpdateViewDeleteView よりも用途が広く、カスタマイズ次第で実現可能なビューも幅広いです。

この FormView の詳細に関しては下記ページで解説を行なっています。

FormViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】FormViewの使い方(クラスベースビューで汎用的なフォームを扱う)

LoginViewLogoutView

LoginView は名前の通りログインを実現する Viewのサブクラス となります。LogoutView も名前の通り、ログアウトを実現する Viewのサブクラス となります

ログインページのビューやログアウトページのビューは、それぞれ LoginViewLogoutView を継承するで簡単に実現することができます。

LoginViewで実現できるページの例

これらの LoginViewLogoutView の詳細に関しては、それぞれ下記ページで解説しています。

LoginViewの使い方の解説ページアイキャッチ 【Django】LoginViewの使い方(クラスベースビューでのログインの実現) LogoutViewの使い方の解説ページアイキャッチ 【Django】LogoutViewの使い方(クラスベースビューでのログアウトの実現)

スポンサーリンク

クラスベースビューのメリット

さて、今回クラスベースビューについて説明してきましたが、このクラスベースビューは今まで利用してきた関数ベースビューに比べてどのようなメリットがあるのでしょうか?

その点について解説していきたいと思います。

処理の実装量が減る

まず、このページの前半でも説明したように、関数ベースビューの場合、実現したいビューに合わせて処理を実装することでビューを作成していくことになります。それに対し、クラスベースビューの場合は、Django フレームワークによって提供される Viewのサブクラス を実現したいビューに合わせてカスタマイズすることで作成することになります。そして、このカスタマイズの多くはクラス変数の定義で行うことができます。

そのため、クラスベースでビューを作成することで、処理の実装量が関数ベースビューに比べて減ります。例えば関数ベースビューでフォームを扱おうとするとリクエストのメソッドの種類に応じて if 文で動作を切り替えるような処理を実装する必要がありますが、クラスベースビューの場合は Viewのサブクラス 側でメソッドの種類に応じた動作の切り替えが行われるようになっているため、そういった処理の実装も不要となります。

ただし、クラスベースビューではメソッドのオーバーライドによってカスタマイズを行うこともあり、この場合は処理の実装が必要となります。が、それでも関数ベースビューに比べれば、クラスベースビューの方が処理の実装量は少なくなります。

実装量が減れば、その分、アプリの開発工数の削減・アプリの開発期間の短縮を行うことが可能となります。

バグが減る

そして、処理の実装量が減ることでバグも減ります

実装する処理が増えたり、処理の複雑度が上がると発生するバグの量も多くなるのが一般的です。そのため、クラスベースビューでビューを作成することによって実装必要な処理を減らしてやれば、その分バグの数も減らすことができると考えられます。そしてこれにより、ウェブアプリの品質も上がることになります。

スポンサーリンク

品質の高さも引き継げる

Viewのサブクラス は Django フレームワークから提供されるクラスであり、Django で開発された世界中のウェブアプリから利用されているクラスです。また、多くの Django ユーザーから利用されています。つまり、Viewのサブクラス は利用実績が非常に多いクラスとなります。そしてその分、品質が高いです。クラスベースビューは、その Viewのサブクラス を継承して作成していくことになるため、Viewのサブクラス の品質の高さに伴って自身で開発するビューも品質が高くなります

それに対し、関数ベースビューのビューはウェブアプリ開発者自身が作成するビューであり、そのビューを実装したウェブアプリからのみ利用されることになります。それだけで品質が低いとは言い切れませんが、それでも品質を高めるためにはウェブアプリ開発者や評価者がしっかりテストを行う必要があります。

もちろん、クラスベースビューの場合もカスタマイズ部分がバグる可能性もありますが、それでも実装する処理の量は減るので、前述の通り関数ベースビューに比べてバグの数は減り、実装量が少ない分バグを見つけやすくもなると思います。

コードの重複が減る

また、関数ベースビューの場合、複数の関数に同じような処理の実装が必要となることが多いです。例えばコメント一覧の表示を行うビューの関数とユーザー一覧の表示を行うビューの関数では、扱うモデルクラスが異なるものの、実装する処理は大体同じになります。例えば下記のような感じになると思います。

コメント一覧とユーザー一覧の表示
from django.shortcuts import render
from .models import Comment, User

def comments_view(request):
    comments = Comment.objects.all()

    context = {
        'comments' : comments
    }

    return render(request, 'forum/comments.html', context)


def users_view(request):
    users = User.objects.all()

    context = {
        'users' : users
    }

    return render(request, 'forum/users.html', context)

同じような処理を実装すれば良いだけなので開発時はそんなに苦にならないかもしれません。ほぼコピペで済みますので…。ですが、こういった同じような処理が複数箇所に存在すると、その処理の中にバグがあると全箇所を修正する必要が出てきます。同じような処理が多ければ多いほど修正が大変になりますし、修正漏れも発生しやすくなります。

なので、こういった同じような処理は関数化するなどして実装箇所が一箇所にまとまるように工夫する必要があります。

ですが、工夫しなくても、こういったことを自然と実現できるのがクラスベースビューとなります。

前述のような関数ベースビューで実装される典型的な処理・重複しがちな処理は Viewのサブクラス で既に実装されています。例えば上記で示したようなインスタンスの一覧を表示する例であれば ListView で実装されています。したがって、Viewのサブクラス を継承して実現したいビューに合わせてカスタマイズしてやることで、自然と実装する処理が重複することを防ぐことができるようになっています。

そして、これによって修正が容易となり、保守性が向上することになります。

クラスベースビューのデメリット

続いて、関数ベースビューと比較した時のクラスベースビューのデメリットについて説明します。

スポンサーリンク

Viewのサブクラス の知識が必要

まず、クラスベースビューを使いこなすためには Viewのサブクラス の知識が必要になります。

クラス変数として何を定義すればクラスの動作がどう変化するのか?

メソッドとして何を定義すればクラスの動作がどう変化するのか?

この辺りの知識がないとクラスベースビューを使いこなすのは難しいと思います。関数ベースビューの場合は、リクエストを受け取り、レスポンスを返却することさえ満たせば、あとは自由に実装して好きなビューを作成することができます。知識よりも実装力の方が重要となります。ですが、クラスベースビューの場合は継承する Viewのサブクラス の知識が必要で、これがないと自由自在にビューを作るようなことができません。

作っている感が減る?

また、ウェブアプリを “作っている感” に関しては関数ベースビューの方が上かなぁと思います。個人的には関数ベースビューの方がビューを作っていて楽しいですね…。自由に好きなように実装し、自身が意図した通りにビューが動作してくれた時はやはり嬉しいです。

ですが、クラスベースビューの場合は単にクラス変数の定義だけでビューが実現できてしまうので逆に物足りなさを少し感じたりもします。まぁ、この辺りは好みかもしれないですね!

もちろん、お試しでウェブアプリを開発する場合などは、関数ベースビューでもクラスベースビューでもどちらでも良いと思いますが、最終的な実装としてはクラスベースビューの方が良いと思います。これは、先ほどメリットで挙げたようにクラスベースビューの方が品質・保守性などが関数ベースビューに比べて高くなるからです。

ということで、最初は関数ベースビューでウェブアプリを開発するのでも良いのですが、最終的にはクラスベースビューも使いこなせるようになっておいた方が良いと思います。

掲示板アプリのビューをクラスベースに変更する

最後に、掲示板アプリのビューをクラスベースに変更する例を示していきたいと思います。

この Django 入門 の連載の中では簡単な掲示板アプリを開発してきており、前回の連載では下記ページで掲示板アプリで発生していた N + 1 問題の解決を行ないました。

DjangoにおけるN+1問題の解説ページアイキャッチ 【Django入門14】N+1問題とselect_related・prefetch_relatedでの解決

今回は、上記ページの 掲示板アプリの N + 1 問題を解決する で示した views.py の全関数をクラスベースビューに置き換えていきたいと思います。変更対象となるファイルは views.pyurls.py (アプリ側)、さらにはテンプレートファイルの base.html のみとなります。

特にポイントになるのが views.py で、この views.py を変更して関数ベースビューをクラスベースビューに置き換えていきます。この views.py でのクラスベースビューへの置き換え時には、Viewのサブクラス の種類 で示した様々な Viewのサブクラス を利用していますので、各 Viewのサブクラス の詳細を知りたい方は、Viewのサブクラス の種類 に設置しているリンク先のページをご参照いただきたいと思います。

また、クラス変数のデフォルト設定 でも説明したように、各 Viewのサブクラス のデフォルト設定に合わせてテンプレートファイルなどの名前を変更してウェブアプリを開発していくやり方もあるのですが、今回は、既に用意しているテンプレートファイル等に合わせて Viewのサブクラス を継承するクラスをカスタマイズしていきたいと思います。

スポンサーリンク

掲示板アプリのプロジェクト一式の公開先

この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。

https://github.com/da-eu/django-introduction

また、前述のとおり、ここでは前回の連載の 掲示板アプリの N + 1 問題を解決する で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-n1

さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。以降では、基本的には前回からの差分のみのコードを紹介していくことになるため、変更後のソースコードの全体を見たいという方は、下記からプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-classview

変更前の views.py

また、後述の解説の中では変更前の views.py の内容を参照するため、変更前の views.py を下記に示しておきます。これらの関数を、1つ1つクラスベースのビューに置き換えていきます。

変更前のviews.py
from django.shortcuts import redirect, render, get_object_or_404
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator

User = get_user_model()

@login_required
def index_view(request):
    return redirect(to='comments')

@login_required
def users_view(request):
    users = User.objects.prefetch_related('comments').order_by('date_joined')

    paginator = Paginator(users, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'forum/users.html', context)

@login_required
def user_view(request, user_id):
    user = get_object_or_404(User, id=user_id)
    comments = Comment.objects.filter(user=user).order_by('date')

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'user' : user,
        'page_obj' : page_obj
    }

    return render(request, 'forum/user.html', context)

@login_required
def comments_view(request):
    comments = Comment.objects.select_related('user').order_by('date')

    paginator = Paginator(comments, 3)
    number = int(request.GET.get('p', 1))
    page_obj = paginator.page(number)

    context = {
        'page_obj' : page_obj
    }

    return render(request, 'forum/comments.html', context)

@login_required
def comment_view(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)

    context = {
        'comment' : comment
    }

    return render(request, 'forum/comment.html', context)

def register_view(request):
    if request.method == 'POST':
        form = RegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('user', user.id)

    else:
        form = RegisterForm()
    
    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)

@login_required
def post_view(request):
    if request.method == 'POST':
        form = PostForm(request.POST)

        if form.is_valid():
            comment = form.instance
            comment.user = request.user
            comment.save()
            
            return redirect('comments')
    else:
        form = PostForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/post.html', context)

def login_view(request):
    if request.method == 'POST':
        form = LoginForm(request, request.POST)

        if form.is_valid():
            user = form.get_user()

            if user:
                login(request, user)
                return redirect('user', user.id)

    else:
        form = LoginForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/login.html', context)

def logout_view(request):
    logout(request)
    return redirect('login')

login_view から LoginView のサブクラスへの置き換え

関数の定義順とは前後しますが、まずは login_view をクラスベースビューに置き換えていきたいと思います。ログインを実現する Viewのサブクラス として LoginView が用意されていますので、この LoginView を継承するクラスを定義し、そのカスタマイズを行うことで login_view 同等のビューを実現していきます。

最初なので、このクラスベースビューへの置き換えについては少し詳しく説明していきたいと思います。

まず、現状の login_view では、メソッドが GET のリクエストを受け取った際に LoginForm クラスのインスタンスをコンテキストの 'form' キーにセットし、 'forum/login.html' のパスにあるテンプレートファイルから HTML を生成してレスポンスとして返却するようになっています。また、メソッドが POST のリクエストを受け取った際には送信されてきたデータに基づいて認証を行い、認証 OK の場合に ③ログインしたユーザーの詳細ページの URL にリダイレクトするようになっています(ユーザーの詳細ページとは /forum/comment/ユーザーID/ の URL のページとなります)。

それに対し、LoginView では、大雑把に説明すれば、メソッドが GET のリクエストを受け取った際に①クラス変数 form_class で指定されるクラスのインスタンスをコンテキストの 'form' キーにセットし、②クラス変数 template_name で指定されるパスにあるテンプレートファイルから HTML を生成してレスポンスとして返却するようになっています。また、メソッドが POST のリクエストを受け取った際には送信されてきたデータに基づいて認証を行い、認証 OK の場合に③クラス変数 success_url で指定される URL or  メソッド get_success_url から返却される URL にリダイレクトするようになっています。

この LoginView の処理の流れで示した①②③の部分を login_view の処理の流れで示した①②③の部分と同等のものになるようにカスタマイズしてやれば、LoginView を継承するクラスで login_view と同等の処理を実現することができるようになります。

まず①と②に関しては、単に LoginView を継承するクラスでクラス変数を下記のように定義してやれば良いことになります。

  • form_class = LoginForm
  • template_name = 'forum/login.html'

ちょっと厄介なのが③です。③では、login_view 同様に、/forum/comment/ユーザーID/ へのリダイレクトを行いたいのですが、ユーザーID にはログインしたユーザーの ID を設定する必要があり、どのユーザーがログインしたかは、実際にユーザーがログインしないと判別できません。つまり、リダイレクト先の URL はアプリ起動時には設定不可で、動的に処理を変化させるためにオーバーライドを利用する で説明したように、このリダイレクト先の URL の設定はメソッドのオーバーライドで実現する必要があります。

今回の場合は、LoginView の持つ get_success_url がオーバーライドの対象であり、具体的には下記のようにメソッドを定義することで、ログインしたユーザーの詳細ページの URL を返却することができるようになります。そして、この返却値に対してリダイレクトが行われることになり、③に関しても login_view 同様のことが実現できることになります。

get_success_urlのオーバーライド
def get_success_url(self):
    return reverse('user', kwargs={'user_id':self.request.user.id})

ということで、login_viewLoginView を継承するクラスに置き換えた結果は下記のようになります。import 部分は省略していますが、後ほど import 部分を含めた views.py 全体のソースコードを紹介しますので、必要な import 処理に関してはそこで確認していただければと思います。

Login
class Login(LoginView):
    form_class = LoginForm
    template_name = 'forum/login.html'

    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.request.user.id})

最初の例としてはちょっと難しかったかもしれませんが、基本的にはここで説明したように、関数ベースビューで行っている処理を整理し、それをクラスベースビューで実現できるようにクラス変数の定義やメソッドのオーバーライドを行なっていくことがクラスベースビューへの置き換えを実現する際の基本的な流れになります。また、実際に関数ベースビューをクラスベースビューに置き換えてみることで、クラスベースビューの仕組みも理解しやすくなると思いますので、是非以降の解説も読み進めていただければと思います。また、自身で定義した関数ベースビューのクラスベースビューへの置き換えに挑戦することでも、クラスベースビューの仕組みの理解を深めることが出来ると思います。

スポンサーリンク

logout_view から LogoutView のサブクラスへの置き換え

続いて logout_view をクラスベースに置き換えていきます。Django フレームワークにはログアウトを実現する LogoutView が用意されており、この LogoutView を利用すればログアウトを行うビューも簡単に実現可能です(そもそも関数ベースでも簡単ですが…)。

まず、logout_view で行っているのはログアウト処理とログインページの URL へのリダイレクトになります。また、LogoutView は動作した際にログアウト処理とクラス変数 next_page へのリダイレクトが行われるようになっています。なので、logout_view と同等の LogoutView を継承するクラスは下記を定義することで実現することができます。

Logout
class Logout(LogoutView):
    next_page = reverse_lazy('login')

reverse_lazy によって、名前 'login' から URL への変換を行い、その変換結果を next_page に指定するようにしています。urls.py でログインページの URL に 'login' という名前を付けるようにしていますので、上記のように next_page を定義しておけば、ログアウト後にログインページにリダイレクトされることになります。また、ログインページの URL は固定ですので、上記のようにクラス変数によってリダイレクト先の URL の設定が可能で、わざわざ Login の時のようにメソッドのオーバーライドを行う必要はありません。

上記で使用している reverse_lazy については下記ページで紹介しているので、詳しく知りたい方はこちらをご参照ください。クラスベースビューのクラス変数の定義時には、reverse ではなく reverse_lazy を利用する必要がある点がポイントとなります。

reverseとreverse_lazyの違いの解説ページアイキャッチ 【Django】reverseとreverse_lazyの違い

とりあえず、上記の Logout により、ログアウトを実現するクラスベースビューが定義できたことになります。ただし、この Logout が継承している LogoutView はメソッドが POST のリクエストしか受け付けないようになっており、メソッドが GET のリクエスト受け取った時には 405 エラーをレスポンスとして返却するようになっています。

それに対し、現状の掲示板アプリでは、ログアウトはナビゲーションバーの ログアウト リンクをクリックしたときに実施されるようになっており、このリンクのクリック時に送信されるリクエストのメソッドは GET となっています。そのため、上記のように logout_viewLogout に置き換えると、ログアウト リンクをクリックしてもログアウトが実施されず、毎回 405 エラーがレスポンスとして返却されることになります。

現状のログアウトリンクからはメソッドがGETのリクエストが送信されるようになっていることを示す図

そのため、上記のように LogoutView を継承するクラスを定義すると同時に、ログアウト リンクがクリックされた時に送信されるリクエストのメソッドが GET ではなく POST になるように変更が必要となります。具体的には、このリンクを出力しているのはテンプレートファイルの base.html ですので、base.html の変更が必要となります。この base.html の変更に関しては、後述の テンプレートファイルの変更 で解説します。

MEMO

Django 4 以前の LogoutView では、リクエストのメソッドが POST でも GET でも受け付けられるようになっており、どちらのメソッドでもログアウトが実施されるようになっていました

LogoutView が受け付け可能なリクエストのメソッドが POST のみに限定されるようになったのは Django 5 以降で、Django 5 以降のバージョンを利用している方は上記のとおり base.html の変更が必須となります

index_view から RedirectView のサブクラスへの置き換え

次は index_view をクラスベースビューに置き換えていきます。

この index_view では単純に、名前が 'comments' に設定された URL へのリダイレクトを行なっているだけになります。

Viewのサブクラス としても単にリダイレクトを行うだけのクラスが用意されており、それが RedirectView となります。

RedirectView は動作する際にクラス変数 url で指定される URL にリダイレクトを行うようになっているため、クラス変数 url を定義し、名前が 'comments' に設定された URL を指定してやれば index_view と同等の動作を実現することができます。その例が下記となります。

Index
class Index(RedirectView):
    url = reverse_lazy('comments')

users_view から ListView のサブクラスへの置き換え

次は users_view のクラスベースビューへの置き換えを行なっていきます。

users_viewUser (CustomUser) のインスタンスの一覧を表示するビューであり、こういったインスタンスの一覧を実現する ViewのサブクラスListView となります。

users_view をクラスベースビューに置き換える上でポイントになるのは下記の4つになると思います。

  • users_view では N + 1 問題の対策としてデータベースに発行するクエリを prefetch_related を利用して生成している
  • users_view ではページネーションが行われている
  • users_view からは 'forum/users.html' のテンプレートファイルが利用されている
  • users_view では @login_required によって非ログインユーザーからのアクセスを禁止している

users_view をクラスベースビューに置き換えるという観点で重要になるのは、これらの users_view で行なっていることがクラスベースのビューでも行われるようにする必要があるという点になります。

クエリの指定

まず1つ目のインスタンスを取得するために発行するクエリの設定は、クラス変数 queryset の定義で実現することができます。users_view の時に発行されていたクエリと同じものが設定されるように、下記のように queryset クラス変数を定義すれば良いです。

クエリ関連のクラス変数
queryset = User.objects.prefetch_related('comments').order_by('date_joined')

ちなみに、order_by は、クエリの発行によって取得されたインスタンスの集合を、引数で指定されたフィールドに対して整列させるためのメソッドになります。ListView では orderering というクラス変数が定義されており、この ordering の定義によっても、同様にインスタンスの整列を行うことが可能です。特に、queryset ではなく model を定義する場合、かつ、インスタンスの集合の整列を行いたい場合は、ordering の定義が必要となります。

ページネーションの設定

また、2つ目のページネーションに関して説明すると、ページネーション自体はクラス変数 paginate_by を定義することで実現することができます。クラス変数 paginate_by を定義して整数を指定しておくことで、各ページに指定した整数分のインスタンスが自動的に割り付けられるようになります。そして、各ページの Page のインスタンスはテンプレートファイルから page_obj という変数名で参照することが可能となります。

また、users_view では表示するページのページ番号をクエリパラメーター 'p' から取得するようになっています。ここも同様のものとするのであれば page_kwarg の定義も必要となります。具体的には、page_kwarg = 'p' を定義しておくことで、users_view と同様にページ番号をクエリパラメーター 'p' から取得するようになります。

そのため、users_view と同様のページネーションを実現するためには、下記のように paginate_bypage_kwarg のクラス変数の定義が必要となります。

ページネーション関連のクラス変数
paginate_by = 3
page_kwarg = 'p'

テンプレートファイルの指定

また、users_view ではテンプレートファイル 'forum/users.html' を利用するようになっているため、クラスベースのビューからも、このファイルを利用するようにカスタマイズを行う必要があります。

これは、利用するテンプレートファイルに合わせてクラス変数 template_name の定義を行えば良いだけになります。users_viewtempalates フォルダからの相対パスで 'forum/users.html' の位置にあるファイルをテンプレートファイルとして利用しているわけですから、下記のように template_name を定義してやれば良いことになります。

テンプレートファイル関連のクラス変数
template_name = 'forum/users.html'

非ログインユーザーのアクセス禁止

また、非ログインユーザーのアクセスからのアクセスの禁止は、クラスベースビューの場合は @login_required の指定ではなく LoginRequiredMixin の継承によって実現することができます。このように、クラス変数やメソッドのオーバーライドだけでなく、ミックスインの継承によってもビューをカスタマイズすることができることは覚えておくと良いと思います。

ちなみに、LoginRequiredMixin によってリダイレクトが行われる場合、リダイレクト先の URL は、settings.py での LOGIN_URL の定義によって設定可能です。これは、@login_required と同様の設定方法となりますので、詳細に関しては下記ページをご参照ください。

ログインの実現方法の解説ページアイキャッチ 【Django入門10】ログイン機能の実現

ということで、users_view と同等のクラスベースビューは、下記のような UserList によって実現することができることになります。

UserList
class UserList(LoginRequiredMixin, ListView):
    queryset = User.objects.prefetch_related('comments').order_by('date_joined')
    template_name = 'forum/users.html'
    paginate_by = 3
    page_kwarg = 'p'

参考:クラスベースビューでのコンテキストの生成

ついでなので、ここでクラスベースビューで生成されるコンテキストについて説明しておきたいと思います。まず、上記の UserList のクラス変数 template_name に指定している 'forum/users.html' は下記のようなテンプレートファイルとなっています。ポイントは、このテンプレートファイルから参照している変数になります。

forum/users.html
{% extends "forum/base.html" %}

{% block title %}
ユーザー一覧
{% endblock %}

{% block main %}
<h2>ユーザー一覧(全{{ page_obj.paginator.count }}人)</h2>
<table class="table table-hover">
   
    <thead>
        <tr>
            <th>ユーザー</th><th>コメント数</th>
        </tr>
    </thead>
    <tbody>
        {% for user in page_obj %}
        <tr>
            <td><a href="{% url 'user' user.id %}">{{ user.username }}</a></td>
            <td>{{ user.comments.all|length }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>
<div class="pagination">
    <span class="step-links">
        {% if page_obj.has_previous %}
            <a href="?p=1">« first</a>
            <a href="?p={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="?p={{ page_obj.next_page_number }}">next</a>
            <a href="?p={{ page_obj.paginator.num_pages }}">last »</a>
        {% endif %}
    </span>
</div>
{% endblock %}

この forum/users.html からは page_obj という変数を参照しています。この page_objPage クラスのインスタンスであることを期待しており、この page_obj のデータ属性を出力することで前後のページ等へのリンクを設定し、さらに page_obj からインスタンス user を取得し、この user のデータ属性を出力することで各ユーザーの情報の表示を実現するようになっています。

また、テンプレートファイルから HTML を生成する際には、テンプレートファイルから参照する変数がコンテキストにセットされている必要があります。forum/users.html の場合は、'page_obj' という変数名で変数を参照しており、この変数は Page のインスタンスである必要があるため、コンテキストの 'page_obj' キーの値として Page のインスタンスがセットされている必要があります。

実際、元々このテンプレートファイルを利用していた users_view では次のような処理を行なっており、これにより上記のようなコンテキストの作成を行なっています。

users_viewのコンテキスト生成
page_obj = paginator.page(number)

context = {
    'page_obj': page_obj
}

では、先ほど示した UserList のような ListView を継承するクラスでは、上記のようにコンテキストを生成する処理を実装する必要はないのでしょうか?

結論としてはケースバイケースとなります。ですが、少なくとも今回の例で扱っている users_view と同等の動作を実現するだけであれば、コンテキストを生成する処理の実装は不要です。なぜなら ListView が、表示するページに対する Page のインスタンスをコンテキストの 'page_obj' キーの値としてセットするように実装されているからです(ページネーションを行う場合のみ)。つまり、上記で示した users_view と同様のコンテキストの生成が行われるように ListView が実装されています。なので、そのサブクラスである UserList においても users_view と同様のコンテキストが生成されるため、わざわざコンテキストを生成するような処理や設定を行わなくても、forum/users.html からは page_obj が参照可能です。

この ListView と同様に、各種 Viewのサブクラス ではコンテキストを生成する処理が実装されており、わざわざ開発者自身がコンテキストを生成する処理を実装することは不要です。ですが、Viewのサブクラス が生成するコンテキストのキー名や、そのキーにセットされる値は Viewのサブクラス によって決められているため、そのキー名や値に合わせてテンプレートファイルを作成する必要があります。そのため、Viewのサブクラス の中で生成されるコンテキストがどのようなものであるのかも知っておいた方がクラスベースビューでの開発が楽になります。この点についても、Viewのサブクラス の種類 で紹介するリンク先の各ページで紹介していますので、是非こちらも読んでみてください。

もしくは、Viewのサブクラス を継承するクラスでクラス変数の定義やメソッドのオーバーライドを行い、コンテキストを自分の好きなように生成するようにカスタマイズするのでも問題ありません。このオーバーライドの実例は、次の user_view から DetailView のサブクラスへの置き換え で紹介します。

スポンサーリンク

user_view から DetailView のサブクラスへの置き換え

次は user_view のクラスベースビューへの置き換えを行なっていきます。

user_viewUser (CustomUser) の1つのインスタンスの詳細を表示するビューであり、こういった1つのインスタンスの詳細の表示を実現する ViewのサブクラスDetailView となります。

DetailView を継承すれば、1つのインスタンスの情報の詳細を表示するビュー自体は簡単に実現することは可能なのですが、user_view と同等のビューを実現するのは結構難しいです。なぜなら、user_view では下の図のように User の1つのインスタンスのみではなく、そのインスタンスと関連付けられている Comment のインスタンスの一覧も表示するようになっているからです。要は、特定のユーザーの詳細情報と、そのユーザーが投稿したコメントの一覧が表示されるようになっています。しかもコメントの一覧はページネーションによってページ分割された状態で表示されるようになっています。

ユーザーの詳細ページに表示される情報の説明図

なので、ビューでは特定の User のインスタンスのみではなく、Comment のインスタンスの集合もコンテキストにセットしてテンプレートに渡す必要があります。それに対し、DetailView は、特定の1つのインスタンスのみをコンテキストにセットするように作られています。そのため、特定の User のインスタンスのみをコンテキストにセットし、このインスタンスの情報のみを表示することは容易に実現することができますが、それだけだと上図のようなページ表示が実現できません。上図のようなページ表示を実現するためには、Comment のインスタンスの集合もコンテキストにセットできるようにカスタマイズを行う必要があります。この辺りが、user_view のクラスベースビューへの置き換えを行う上でのポイントになると思います。

特定の User のインスタンスのみの表示

まずは特定の User のインスタンスの詳細情報の表示のみを実現するビューを作成し、その次に Comment のインスタンスの一覧を表示するようビューを変更するという流れで、段階的に解説していきたいと思います。

特定の User のインスタンスの詳細情報の表示のみを実現するビューは下記の UserDetail によって実現することが可能です。LoginRequiredMixin を継承しているのは、前述の通り、非ログインユーザーからのアクセスを禁止するためです。

UserDetail(Userのみ)
class UserDetail(LoginRequiredMixin, DetailView):
    model = User
    pk_url_kwarg = 'user_id'
    template_name = 'forum/user.html'

まず、クラス変数 model によってインスタンス(レコード)の取得先のモデルクラス(テーブル)の指定を行なっています。 model = User と定義しているため、 UserDetail が動作する際には User (正確には CustomUser) のテーブルから1つのインスタンスが取得されることになります。

また、DetailView は、その取得するインスタンスを特定するための条件が URL で指定されることを前提とした作りとなっています。これは関数ベースビューの場合も同様で、user_view に対するurlpatterns の要素は下記のように指定されており、URL における user/<int:user_id>/<int:user_id> 部分に指定された整数が、user_view 関数に引数 user_id として渡されるようになっています。そのため、user_view では引数 user_id をインスタンスを特定するための情報として利用し、 1つのインスタンスのみを取得することができるようになっています。

関数ベースビューの時のurls.py
from django.urls import path
from . import views

urlpatterns = [
    # 略
    path('user/<int:user_id>/', views.user_view, name='user'),
    # 略
]

DetailView の場合も同様で、上記と同等の urlpatterns を定義した場合、<int:user_id> 部分に指定された整数が DetailView に引数 user_id として渡されるようになっています。ですが、引数受け取り側の DetailView では、インスタンスを特定するための整数を引数 pk として受け取ることを前提とした作りになっています。つまり、urlpatternsDetailView とで話が合っていないことになります。この場合、DetailViewが動作した際に例外が発生することになります。

値をプライマリーキーとして扱うキー名がViewとurls.pyとで食い違っている様子

ただし、インスタンスを特定するための整数を受け取る引数の引数名は pk から変更可能です。この引数名を変更するために定義するクラス変数が pk_url_kwarg で、このクラス変数を定義することで、この引数名を pk から変更することが可能です。UserDetail では pk_url_kwarg = 'user_id' を定義しているため、この引数名が user_id となり、URL で <int:user_id> 部分に指定された整数を UserDetail が受け取ることができるようになります。

値をプライマリーキーとして扱うキー名がViewとurls.pyとで話が合っている様子

そして、UserDetail は、受け取った整数とプライマリーキーが一致するインスタンスを User のテーブルから取得することになります。

解説を読んで気づかれた方もおられるかもしれませんが、わざわざ pk_url_kwarg を定義しなくても、urlpatterns の中で実行している path 関数の第1引数における <int:user_id> 部分を <int:pk> に変更するのでもオーケーです。大事なのは、ビューと urls.py とで話が合うように、それぞれのファイルを実装することです。

あとは、template_nameuser_view から利用しているテンプレートファイルに合わせて定義してやれば、特定のユーザーの詳細情報を表示するページは完成することになります。

Comment の一覧の追加

次は、取得した User のインスタンスと関連付けられている Comment のインスタンスの一覧の表示を行うようにしていきたいと思います。最初に結論を言えば、下記のように UserDetailget_context_data メソッドの定義を追加することで、この Comment のインスタンスの一覧の表示を実現することが可能です。

UserDetail
class UserDetail(LoginRequiredMixin, DetailView):
    model = User
    pk_url_kwarg = 'user_id'
    template_name = 'forum/user.html'

    def get_context_data(self, **kwargs):
        comments = Comment.objects.filter(user=self.object).order_by('date')

        paginator = Paginator(comments, 3)
        number = int(self.request.GET.get('p', 1))
        page_obj = paginator.page(number)

        return super().get_context_data(page_obj=page_obj)

ここで定義した get_context_dataDetailView の持つメソッドで、コンテキストを生成して返却するメソッドとなっています。DetailViewget_context_data では可変個のキーワード引数を受け取れるようになっており、下記のように各キーにデータをセットしたコンテキストを生成するようになっています。

  1. 'object': model のインスタンス
  2. 'modelに指定したモデルクラスの名前(小文字)': model のインスタンス
  3. 'キーワード引数 1 の引数名': キーワード引数 1 の値
  4. 'キーワード引数 2 の引数名': キーワード引数 2 の値
  5. (以下、同様にして指定されたキーワード引数に応じてデータがセットされる)

ただし、この get_context_dataDetailView で定義される get メソッドから実行されるようになっており、その際にはキーワード引数が指定されないようになっています。したがって、コンテキストにセットされるデータは、上記における 1. と 2. の model のインスタンスのみとなります。なので、UserDetail の場合は User のインスタンスしかテンプレートファイルから参照できないことになります。これだとテンプレートファイルからは Comment のインスタンスが参照できません…。

ですが、前述の通り get_context_data 自体は可変個のキーワード引数を受け取れるようになっており、そこで指定した引数に応じて、上記の 3. 以降のようにコンテキストにデータがセットされるようになっています。そのため、get_context_data をオーバーライドし、コンテキストにセットしたいデータを用意してキーワード引数を指定した状態でスーパークラス(DetailView)のメソッドを実行するようにすれば、任意のデータをコンテキストにセットできることになります。

それを行なっているのが上記の UserDetailget_context_data で、UserDetailget_context_data では、user=self.object を満たす Comment のインスタンスの集合を取得し、そのインスタンスの集合からページネーションを行い、クエリパラメーター 'p' で取得されるページ番号の Page のインスタンス page_obj を生成するようになっています(self.object には User のインスタンスがセットされています)。

さらに、その生成した page_obj をキーワード引数に指定する形でスーパークラス、つまり DetailViewget_context_data を実行してコンテキストの生成を行なっています。この際、キーワード引数で page_obj=page_obj を指定しているため、下記のように各キーにデータがセットされたコンテキストが生成されることになります。

  • 'object': User のインスタンス
  • 'user': User のインスタンス
  • 'page_obj': page_obj (Page のインスタンス)

これにより、user_view が利用していたテンプレートファイルから参照される User のインスタンスと Page のインスタンスの両方を含むコンテキストが用意できることになり、これによって User のインスタンスの詳細情報と、その User のインスタンスと関連付けられた Comment のインスタンスの一覧を表示する HTML が生成できることになります。

上記の get_context_data のオーバーライドの例は少し無理矢理なものになるかもしれませんが、メソッドのオーバーライドを利用すれば様々なコンテキストの作成が実現できることは理解していただけたのではないかと思います。

また、下記ページの extra_context の節でも説明していますが、コンテキストへの要素の追加はクラス変数 extra_context の定義によっても実現可能です。下記は ListView の解説ページとなりますが DetailView でもクラス変数 extra_context の定義によってコンテキストへの要素の追加が可能です。

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

ただし、今回はコンテキストにセットする Page のインスタンスを動的に変化させる必要があったため、もっと具体的に言えばクエリパラメーター p で指定されるページ番号に応じて Page のインスタンスを動的に変化させる必要があったため、get_context_data のオーバーライドによってコンテキストに追加する要素を動的に設定するようにしています。

comments_view から ListView のサブクラスへの置き換え

comments_view のクラスベースビューへの置き換えに関しては users_view から ListView のサブクラスへの置き換え とほぼ同様であるため、解説は省略します。

comments_view と同等のクラスベースビューは下記の CommentList によって実現することができます。

CommentList
class CommentList(LoginRequiredMixin, ListView):
    queryset = Comment.objects.select_related('user').order_by('date')
    paginate_by = 3
    page_kwarg = 'p'
    template_name = 'forum/comments.html'

comment_view から DetailView のサブクラスへの置き換え

comment_view のクラスベースビューへの置き換えに関しても user_view から DetailView のサブクラスへの置き換え とほぼ同様なので、この解説に関しても省略します。ただ、comment_view の場合は Comment のインスタンスの情報さえ表示されば良いようになっているため、get_context_data のオーバーライドは不要となります。

comment_view と同等のクラスベースビューは下記の CommentDetail によって実現することができます。

CommentDetail
class CommentDetail(LoginRequiredMixin, DetailView):
    model = Comment
    pk_url_kwarg = 'comment_id'
    template_name = 'forum/comment.html'

スポンサーリンク

register_view から CreateView のサブクラスへの置き換え

続いて register_view のクラスベースビューへの置き換えを行なっていきます。

register_view はユーザー登録を行うためのビューになっています。このユーザー登録は User (CustomUser) のインスタンスのデータベースへの新規登録によって実現することができ、こういったインスタンスの新規登録を実現する ViewのサブクラスCreateView となります。

form_class の定義

こういったインスタンスの新規登録を行う際には、利用者から新規登録するインスタンスの情報をフォームで入力してもらうことが多いです。そして、そのフォームから送信されたデータに従ってインスタンスのデータベースへの新規登録を行います。

そのため、CreateView を継承するクラスでは、クラス変数 form_class を定義し、このクラス変数で利用するモデルフォームクラスを指定する必要があります。モデルフォームに関しては下記ページで解説しているため、詳しくは下記ページを参照してください。

Djangoのモデルフォーム解説ページアイキャッチ 【Django入門8】モデルフォームの基本

register_view ではモデルフォームクラスとして RegisterForm を利用しているため、register_view の代わりとする CreateView のサブクラスでは form_class = RegisterForm を定義してやれば良いことになります。RegisterFormUser (CustomUser) をベースとするモデルフォームとなります。

この form_class の定義により、CreateView を継承するクラスがメソッド GET のリクエストを受け取った際には form_class で指定されたモデルフォームクラスに基づいたフォームがページに表示されることになります。さらに、メソッド POST のリクエストによってフォームからデータが送信されてきた際には、まず送信されてきたデータの妥当性の検証が行われます。検証結果 OK の場合は、送信されてきたデータがレコードとして、form_class に指定されたモデルフォームクラスのベースのモデルクラスに対応するテーブル、つまり、今回の場合は User のテーブルに新規保存されることになります。そして、レコードの新規保存後に特定の URL へのリダイレクトが行われます。

この辺りの一連の処理の流れは基本的には register_view と同様であり、これらは CreateView で実装されているため、この CreateView を継承するだけで、上記のような処理の流れも簡単に実現することができることになります。

form_valid メソッドの定義

ただし、register_view では、妥当性の検証結果が OK の場合に User のテーブルへのレコードの新規保存を行うだけでなく、その User のインスタンスに対するログイン処理も実施されるようになっています。

ですが、CreateView ではレコードの新規保存が行われるだけで、ログイン処理は実施されません。こういった、CreateView に実装されていない処理を追加で実施したいような場合はクラスのカスタマイズが必要になります。

今回のような、妥当性の検証結果が OK の場合の処理の追加に関しては、form_valid メソッドのオーバーライドにより実現することができます。CreateViewform_valid は、フォームから送信されてきたデータに対する妥当性の検証結果が OK の場合に実行されるメソッドであり、レコードの新規保存、および、クラス変数 success_url で指定される URL  or get_success_url メソッドから返却される URL へのリダイレクトのレスポンスの返却のみが行われるようになっています。

なので、form_valid をオーバーライドし、これらの処理に追加してログイン処理を実施するようにしてやれば良いことになります。具体的には、下記のような form_valid でオーバーライドを行えば良いです。

form_valid
def form_valid(self, form):
    response = super().form_valid(form)
    user = self.object
    login(self.request, user)
    return response

1行目がスーパークラス(CreateView)の form_valid の実行で、これによってインスタンスがデータベースにレコードとして保存されることになります。次の2行目では、1行目で保存されたインスタンスの取得を行っています。CreateView を継承するクラスの場合、保存されたインスタンスは self.object から取得可能で、form_class で指定したモデルフォームクラスのベースとなっているモデルクラスのインスタンスが取得できます。具体的には、form_class = RegisterForm を定義して場合、User のインスタンスが取得されることになります。

User のインスタンスが取得できれば、後は取得した User のインスタンスを引数に指定して login 関数を実行してログインを行い、最後にスーパークラスの form_valid の返却値を返却すれば、スーパークラスの form_valid の処理に加えてログイン処理が追加で実行されるクラスに仕立てることができます。

このように、form_valid のオーバーライドを行うことで、妥当性の検証結果が OK の時に実行する処理をカスタマイズすることが可能です。

ということで、register_view 同等のクラスベースビューは下記の Register によって実現することができます。詳細な説明は省略させていただいていますが、register_view でも login_view 同様に、ログイン後にログインユーザーの詳細情報を表示するページへのリダイレクトが行われるようになっていますので、login_view から LoginView のサブクラスへの置き換え で説明した内容と同様に、そのリダイレクト先の URL の取得を get_success_url のオーバーライドによって実現しています。

Register
class Register(CreateView):
    model = User
    form_class = RegisterForm
    template_name = 'forum/register.html'

    def form_valid(self, form):
        
        response = super().form_valid(form)
        user = self.object
        login(self.request, user)
        return response
    
    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.object.pk})

post_view から CreateView のサブクラスへの置き換え

ビューの変更の最後に post_view のクラスベースビューへの置き換えを行なっていきます。

post_view ではフォームから送信されてきたデータに応じた Comment のインスタンスのデータベースへの新規登録が行われるようになっています。新規登録を行うわけですから、先ほど紹介した Register 同様に CreateView を継承してクラスを定義することで、この post_view と同等のクラスベースビューも実現することができます。なので、基本的には register_view から CreateView のサブクラスへの置き換え を参考にしてクラスを定義していけば良いことになります。

ですが、post_view の場合、Comment のインスタンスをデータベースに保存する前に、そのインスタンスにログインユーザーの User のインスタンスを関連付ける必要があります。要は、コメントの投稿者が誰であるかを設定した後に、データベースへの保存を行う必要があります。CreateView では、基本的にフォームから送信されてきたデータに基づいてインスタンスの生成が行われるため、こういったフォームから送信されてこないデータの設定等は form_valid のオーバーライドによって実現する必要があります(インスタンスの新規登録時の日付の設定などは自動的に行われるようにモデルクラスを定義するようなこともできます)。

具体的には、下記のような form_valid を定義してオーバーライドすれば、データベースに保存しようとしている Comment のインスタンスとログイン中ユーザーのインスタンス(self.request.user)との関連付けを行った後に、その Comment のインスタンスがデータベースに新規登録されるような動作を実現することができます。

form_valid
def form_valid(self, form):
    form.instance.user = self.request.user
    return super().form_valid(form)

また、post_view の場合、Comment のインスタンスのデータベースへの新規登録が完了した後にコメントの一覧を表示するページへのリダイレクトが行われるようになっています。毎回同じページへのリダイレクトが行われるわけですから、URL は静的に決まることになり、リダイレクト先の URL はアプリ起動時に設定可能です。そのため、get_success_url メソッドのオーバーライドではなく、クラス変数 success_url の定義によって、リダレクト先の URL の設定を行うことができます。

この辺りを踏まえると、post_view 同等のクラスベースビューは下記の Post によって実現することができます。

Post
class Post(LoginRequiredMixin, CreateView):
    form_class = PostForm
    template_name = 'forum/post.html'
    success_url = reverse_lazy('comments')

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

ビューの変更点まとめ

以上で、views.py が全てクラスベースビューに置き換わったことになります。

ここまで個別に各クラスを紹介してきたため、最後に import 部分等も含めて変更後の views.py 全体を示しておきます。

views.py
from .forms import RegisterForm, PostForm, LoginForm
from .models import Comment
from django.contrib.auth import login
from django.contrib.auth import get_user_model
from django.core.paginator import Paginator
from django.views.generic import ListView, DetailView, RedirectView, CreateView
from django.contrib.auth.views import LoginView, LogoutView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse, reverse_lazy

User = get_user_model()

class Login(LoginView):
    form_class = LoginForm
    template_name = 'forum/login.html'

    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.request.user.id})

class Logout(LogoutView):
    next_page = reverse_lazy('login')

class Index(RedirectView):
    url = reverse_lazy('comments')

class UserList(LoginRequiredMixin, ListView):
    queryset = User.objects.prefetch_related('comments').order_by('date_joined')
    template_name = 'forum/users.html'
    paginate_by = 3
    page_kwarg = 'p'

class UserDetail(LoginRequiredMixin, DetailView):
    model = User
    pk_url_kwarg = 'user_id'
    template_name = 'forum/user.html'

    def get_context_data(self, **kwargs):
        comments = Comment.objects.filter(user=self.object).order_by('date')

        paginator = Paginator(comments, 3)
        number = int(self.request.GET.get('p', 1))
        page_obj = paginator.page(number)

        return super().get_context_data(page_obj=page_obj)


class CommentList(LoginRequiredMixin, ListView):
    queryset = Comment.objects.select_related('user').order_by('date')
    paginate_by = 3
    page_kwarg = 'p'
    template_name = 'forum/comments.html'


class CommentDetail(LoginRequiredMixin, DetailView):
    model = Comment
    pk_url_kwarg = 'comment_id'
    template_name = 'forum/comment.html'

class Register(CreateView):
    model = User
    form_class = RegisterForm
    template_name = 'forum/register.html'

    def form_valid(self, form):
        
        response = super().form_valid(form)
        user = self.object
        login(self.request, user)
        return response
    
    def get_success_url(self):
        return reverse('user', kwargs={'user_id':self.object.pk})

class Post(LoginRequiredMixin, CreateView):
    form_class = PostForm
    template_name = 'forum/post.html'
    success_url = reverse_lazy('comments')

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

スポンサーリンク

テンプレートファイルの変更

続いてテンプレートファイルの変更を行なっていきます。ここで変更するのは base.html のみとなります。

logout_view から LogoutView のサブクラスへの置き換え でも解説したように、Logout が受け付け可能なリクエストのメソッドは POST のみとなります。ですが、現状の掲示板アプリでは、ログアウトはナビゲーションバーの ログアウト リンクをクリックしたときに実施することを想定したつくりとなっており、このリンクのクリック時に送信されるリクエストのメソッドは GET となっています。なので、logout_view から Logout に置き換えると ログアウト リンクのクリック時に必ずエラーが発生し、ログアウトを実施することができなくなります。

現状のログアウトリンクからはメソッドがGETのリクエストが送信されるようになっていることを示す図

この ログアウト リンクのクリック時に、Logout が受け付け可能な “メソッドが POST のリクエスト” が送信されるよう、変更を加えていきたいと思います。

まず、上図のようなナビゲーションバーは、テンプレートファイルの forum/templates/forum/base.html で実装されています。そして、この base.html における、ログアウト リンクの出力部分は、現状下記のようになっています。

現状のログアウトリンク
<li class="nav-item">
    <a class="nav-link" href="{% url 'logout' %}">ログアウト</a>
</li>

このように、a タグを利用してリンクを出力しているのですが、a タグで出力するリンクのクリック時にメソッドが POST のリクエストを送信させるようにするためには少し特殊なことを行う必要があるため、ここでは a タグは利用せず、form タグを利用して、単なるリンクではなく「入力フィールドの存在しないフォーム」を出力するようにしたいと思います。つまり、ボタンのみ存在するフォームを出力させます。そして、ボタンには ログアウト という文字列を表示させます。

そのために、上記のログアウトリンク出力部分を下記のように変更します。form タグの場合、下記のように、method 属性で簡単にボタンクリック時に送信されるメソッドを設定することが可能です。

フォームへの変更後のログアウトリンク
<li class="nav-item">
    <form id="logout-form" method="post" action="{% url 'logout' %}">
        {% csrf_token %}
        <button type="submit">ログアウト</a>
    </form>
</li>

Django では、下記ページで解説している CSRF 対策の仕組みが存在するため、フォーム出力時には必ず {% csrf_token %} を記述する必要があるという点に注意してください。

DjangoにおけるCSRFトークンやCSRF検証についての説明ページアイキャッチ 【Python/Django】CSRF対策について解説

上記のように変更することで、ナビゲーションバーが下の図のように変化し、ログアウト ボタンのクリック時にはメソッドが  POST のリクエストが送信され、無事 Logout が受付可能なリクエストが送信されるようになります。そして、そのリクエストを受け取った Logout によってログアウトが実施されるようになります。

ログアウトリンクをフォームに変更したときのナビゲーションバー

ただ、このままだと違和感があるので、もともとの ログアウト リンクと同じ見た目になるようにボタンの CSS の設定を行いたいと思います。これは、下記のように ログアウト ボタンの出力を行っている button タグに class 属性を指定して bootstrap で定義されているスタイルを適用することで実現できます。

見た目変更後のログアウトリンク
<li class="nav-item">
    <form id="logout-form" method="post" action="{% url 'logout' %}">
        {% csrf_token %}
        <button class="btn btn-link nav-link" type="submit">ログアウト</a>
    </form>
</li>

以上の変更により、ナビゲーションバーが下の図のような見た目に戻りますし、前述のとおり、この ログアウト をクリックすることでログアウトが実施されるようになります。

ボタンの見た目を他のリンクと同じに変更したナビゲーションバー

最後に、変更後の base.html 全体を下記に記しておきます。

変更後のbase.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    <header>
        <nav class="navbar navbar-expand navbar-dark bg-primary">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <a class="nav-link navbar-brand" href="{% url 'index' %}">掲示板</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'users' %}">ユーザー一覧</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'comments' %}">コメント一覧</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'post' %}">コメント投稿</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'register' %}">ユーザー登録</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'login' %}">ログイン</a>
                </li>
                <li class="nav-item">
                    <form id="logout-form" method="post" action="{% url 'logout' %}">
                        {% csrf_token %}
                        <button class="btn btn-link nav-link" type="submit">ログアウト</a>
                    </form>
                </li>
            </ul>
        </nav>
    </header>
    <main class="container my-5 bg-light">
        {% block main %}{% endblock %}
    </main>
</body>
</html>

URL とクラスベースビューのマッピング

最後に、アプリがリクエストを受け取った際に、ここで定義したクラスベースのビューが実行されるように urls.py の変更を行いたいと思います。変更の仕方は urls.py を作成する で示した通りで、関数ベースビューの場合は urlpatterns の要素に指定する path 関数の第2引数に 関数オブジェクト をそのまま指定する必要がありましたが、クラスベースビューの場合は path 関数の第2引数には クラスオブジェクト.as_view() を指定する必要があります。

要は、変更前の urls.py における 関数オブジェクト の部分を、置き換え後のクラスの クラスオブジェクト.as_view() に書き換えてやれば良いです。

変更前の urls.py は下記のようになっており、各 path 関数の第2引数には views.index_view などの関数オブジェクトを指定しているため、これらを views.Index.as_view() などのクラスオブジェクトの as_view メソッド実行結果に置き換えてやれば良いです。

変更前のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.index_view, name='index'),
    path('comments/', views.comments_view, name='comments'),
    path('comment/<int:comment_id>/', views.comment_view, name='comment'),
    path('users/', views.users_view, name='users'),
    path('user/<int:user_id>/', views.user_view, name='user'),
    path('register/', views.register_view, name='register'),
    path('post/', views.post_view, name='post'),
    path('login/', views.login_view, name='login'),
    path('logout/', views.logout_view, name='logout'),
]

具体的には下記のように変更してやれば、第1引数で指定した URL (URL パターン) へのリクエストを受け取った際に、第2引数で指定したクラスが動作することになります。

変更後のurls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.Index.as_view(), name='index'),
    path('comments/', views.CommentList.as_view(), name='comments'),
    path('comment/<int:comment_id>', views.CommentDetail.as_view(), name='comment'),
    path('users/', views.UserList.as_view(), name='users'),
    path('user/<int:user_id>', views.UserDetail.as_view(), name='user'),
    path('register/', views.Register.as_view(), name='register'),
    path('post/', views.Post.as_view(), name='post'),
    path('login/', views.Login.as_view(), name='login'),
    path('logout/', views.Logout.as_view(), name='logout'),
]

以上で、関数ベースビューからクラスベースビューへの置き換えは完了です。動作確認の説明は省略しますが、是非、クラスベースに置き換えたビューで、関数ベースビューの時同様の動作が実現できていることを確認してみていただければと思います。

まとめ

このページでは、クラスベースビューについて解説しました!

クラスベースビューとはクラスの定義によって作成されるビューです。それに対し、関数の定義によって作成されるビューは関数ベースビューと呼ばれます。クラスベースビューは品質や保守性の面で関数ベースビューよりも優れています。

また、クラスベースビューは Django フレームワークで定義される Viewのサブクラス を継承するクラスを定義し、開発するウェブアプリに合わせてカスタマイズしていくことで実装していくことになります。Viewのサブクラス には典型的なビューの処理の流れが実装されており、この処理の流れをクラス変数の定義やメソッドのオーバーライドによって開発するウェブアプリに応じたものにカスタマイズすることができるようになっています。

ただし、Viewのサブクラス によって定義されているクラス変数やメソッドが異なるため、Viewのサブクラス によってカスタマイズの仕方が異なることになります。なので、各 Viewのサブクラス についても知識があったほうが良いです。この辺りは、Viewのサブクラス ごとに解説ページを設けて説明していますので、興味があれば下記ページも読んでみていただければと思います!

DjangoのListViewの解説ページアイキャッチ 【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現) DjangoのDetailViewの解説ページアイキャッチ 【Django】DetailViewの使い方(クラスベースビューでの詳細ページの実現) CreateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】CreateViewの使い方(クラスベースビューでの新規登録ページの実現) UpdateViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】UpdateViewの使い方(クラスベースビューでのレコード更新ページの実現) DeleteViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】DeleteViewの使い方(クラスベースビューでのレコード削除ページの実現) FormViewでのクラスベースビューの実現方法解説ページアイキャッチ 【Django】FormViewの使い方(クラスベースビューで汎用的なフォームを扱う) LoginViewの使い方の解説ページアイキャッチ 【Django】LoginViewの使い方(クラスベースビューでのログインの実現) LogoutViewの使い方の解説ページアイキャッチ 【Django】LogoutViewの使い方(クラスベースビューでのログアウトの実現)

同じカテゴリのページ一覧を表示