このページでは、Django のビューの1つの種類である「クラスベースビュー」について解説していきます!
Contents
- クラスベースビュー
- クラスベースビューの作り方
- Viewのサブクラス の種類
- クラスベースビューのメリット
- クラスベースビューのデメリット
- 掲示板アプリのビューをクラスベースに変更する
- 掲示板アプリのプロジェクト一式の公開先
- 変更前の views.py
- login_view から LoginView のサブクラスへの置き換え
- logout_view から LogoutView のサブクラスへの置き換え
- index_view から RedirectView のサブクラスへの置き換え
- users_view から ListView のサブクラスへの置き換え
- user_view から DetailView のサブクラスへの置き換え
- comments_view から ListView のサブクラスへの置き換え
- comment_view から DetailView のサブクラスへの置き換え
- register_view から CreateView のサブクラスへの置き換え
- post_view から CreateView のサブクラスへの置き換え
- ビューの変更点まとめ
- テンプレートファイルの変更
- URL とクラスベースビューのマッピング
- まとめ
クラスベースビュー
まずは、クラスベースビューとは何なのか?この点について解説していきたいと思います。
クラスベースビューとは
クラスベースビューとは、ビューの種類の1つです。ビューは、Django の基本構成となる MTV における V となります。
そして、クラスベースビューとは、クラスの定義によって作成されるビューのことを言います。
その他のビューの種類としては「関数ベースビュー」が挙げられます。この Django 入門 の連載においては、下記ページでビューの解説を行いましたが、ここで紹介したビューは関数ベースビューとなります。また、Django 入門 の連載の中で開発してきている掲示板アプリのビューに関しても関数ベースビューとなります。
【Django入門3】ビュー(View)の基本とURLのマッピングスポンサーリンク
クラスベースビューと関数ベースビューの違い
では、このクラスベースビューと関数ベースビューの違いとは何なのでしょうか?
ビューの作り方が異なる
この2つではビューの作り方、言い換えれば views.py
に定義する対象が異なります。
関数ベースビューでは、views.py
に関数を定義することでビューを作成していくことになります。それに対し、クラスベースビューの場合は、前述の通り views.py
にクラスを定義することでビューを作成していくことになります。その名の通り、関数ベースビューとは関数のビューであり、クラスベースビューとはクラスのビューとなります。
もっと詳細に言えば、関数ベースビューの場合、views.py
へ定義する関数に処理を実装していくことでビューを作成していきます。クラスベースビューの場合、views.py
に Django フレームワークが提供するクラスのサブクラスを定義し、そのサブクラスのカスタマイズを行うことでビューを作成していきます。
このカスタマイズは、クラス変数の定義やメソッドのオーバーライドによって実現されます。オーバーライドを行う場合はクラスベースビューでも処理を実装することになりますが、それもサブクラスのカスタマイズの一種であると捉えていただければ良いです。
関数ベースビューの例
次は簡単な例を確認しながら、関数ベースビューとクラスベースビューの違いについて考えていきたいと思います。
例えば下記の views.py
における comments_view
は関数を定義することで作成されているため、この comments_view
は「関数ベースビュー」であると考えられます。
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.py
の comments_view
関数と同じものになります。
この comments_view
関数はリクエストを受け取り、Comment.objects.all()
により Comment
というモデルクラス(テーブル)から全インスタンス(レコード)を取得するクエリを生成し、それをテンプレートファイルから comments
という名前で参照できるようにコンテキスト context
にセットします。
さらに、comments_view
関数は引数にテンプレートファイル 'forum/comments.html'
とコンテキスト context
を指定して render
関数を実行し、その実行結果を return
するようになっています。これにより、render
関数でテンプレートファイルとコンテキストから HTML が生成され、その結果がレスポンスとして Django フレームワークに返却されることになります。あとは、そのレスポンスが Django フレームワークを経由してクライアントに返却され、このレスポンスの返却により、クライアント側でページの表示が行われることになります。
このサイトの Django 入門 の連載を読んでくださっている方であれば、上記はお馴染みの処理の流れになると思います。ここで重要なポイントは、前述の通り、上記の views.py
のような関数ベースのビューにおいては実現したい処理の流れを実装する必要があるという点になります。
クラスベースビューの例
さて、先ほど示した comments_view
は関数ベースビューとなります。それに対し、comments_view
をクラスベースビューに置き換えた場合の views.py
は下記のようになります。この views.py
における CommentList
は、先ほど示した関数ベースビューの comments_view
と同じページの表示を実現するクラスとなります。
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
における get
メソッドの処理の流れを表したものであり、細かく言えば、このメソッドの処理の流れはもっと複雑です
例えば、④で示した処理は ListView
における get
メソッドが終了した後に実行されるようになっています
が、まずは大雑把に上記のような処理の流れを捉えていただいた方が、関数ベースビューとクラスベースビュの違いや関係性について理解しやすいと思います
この図の青字で示した model
・context_object_name
・template_name
は ListView
のクラス変数となります。そして、ListView
を継承したクラスから、これらのクラス変数の定義を上書きすることができるようになっています。
また、ListView
を継承することで、先ほど処理の流れを示した get
メソッドも CommentList
に継承されることになります。そして、この get
メソッドは各種クラス変数の定義に従って動作するようになっています。そのため、クラス変数の定義を変更してやれば、それに伴って get
メソッドの処理・動作が変化することになります。
例えば、model = Comment
と定義しておけば、②では Comment.objects.all()
が実行されることになります。model = User
と定義しておけば、②では User.objects.all()
が実行されることになります。
したがって、下記のように ListView
を継承する CommentList
クラスを定義すれば、
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
クラスでは、model
が Comment
、context_object_name
が 'comments'
、template_name
が 'forum/comments.html'
として扱われるようになるため、この CommentList
の get
メソッドの処理の流れは下図のようになります。
そして、この処理の流れは comments_view
と同様のものであることが確認できると思います。つまり、ListView
には comments_view
で行っていたものと同様の処理が既に定義されており、その処理をクラス変数によってカスタマイズできるようになっています。上記の CommentList
は、クラス変数を変更することで comments_view
と同様の処理を実現するためのカスタマイズ例の1つとなります。
是非ここで覚えておいていただきたいのが、Django フレームワークには関数ベースビューで実装される典型的な処理の流れを実現するためのクラスが既に数多く用意されているという点になります。これらは全て View
のサブクラスとして定義されており、その例の1つが ListView
となります。実際には、これらのクラスは単純に View
を継承しているだけでなく、様々なクラスを継承して実現されているのですが、最終的には View
を継承しています。そして、こういった View
を継承しているクラスのことを、このサイトでは Viewのサブクラス
と呼ばせていただきます。
例えば、特定のモデルクラス
のインスタンスを全て取得し、それらを 特定のテンプレートファイル
に埋め込んで表示するような処理は “関数ベースビューで実装される典型的な処理の流れ” と言ってよいでしょう。皆さんもそんなビューを実装したことがあるのではないでしょうか?
まさに、その例の1つが comments_view
となります。
そして、このような典型的な処理の流れは Django フレームワークで ListView
によって既に実装されています。そして、この ListView
のサブクラスを定義してカスタマイズしてやることで、特定のモデルクラス
の部分や 特定のテンプレートファイル
の部分を好きなように変更したビューを実現することができます。
“関数ベースビューで実装される典型的な処理の流れ” は他にもあって、例えば 特定のモデルクラス
における 特定の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 パターン) へのリクエストをウェブアプリが受け取った際に実行させたい関数オブジェクトを指定する必要がありました。
from django.urls import path
from . import views
urlpatterns = [
path(URL, views.関数名),
略
]
クラスベースビューの場合も基本的な形式は関数ベースビューの時と同様となるのですが、path
の第2引数には、第1引数の URL (URL パターン) へのリクエストをウェブアプリが受け取った際に動作させたいクラスの as_view
メソッドの実行結果を指定する必要があります。つまり、views.クラス名.as_view()
を指定する必要があります。
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入門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のサブクラス
のメソッドの処理・動作が変化することになります。
クラスベースビューの例 でも、ListView
のサブクラスでクラス変数 model
を Comment
として定義することで ListView
の get
メソッドでのレコードの取得先のテーブルが Comment
に設定される例を示しましたが、クラス変数 model
を別のモデルクラスとして定義すれば、それに伴って ListView
の get
メソッドでのレコードの取得先のテーブルも変化することになります。
このように動作が変化するのは、ListView
の get
メソッドで、レコードの取得先のテーブルとしてクラス変数 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_suffix
:template_name
が指定されなかった場合の、HTML 生成時に使用されるテンプレートファイルのファイル名のサフィックス
* を付けたクラス変数はページネーションに関わる設定になります。ページネーションとは多数のレコードを複数のページに割り付けて表示する機能であり、別途下記ページで解説をしていますので、詳しく知りたい方は下記ページをご参照ください。
【Django入門13】ページネーションの基本前述の通り、これらのクラス変数を ListView
のサブクラスに定義することで、ListView
の get
メソッドの動作を変化させることが可能となります。ですが、上記のクラス変数を定義しても意味のない Viewのサブクラス
も存在します。
例えば Viewのサブクラス
には DetailView
という1つのレコードの詳細情報を表示するクラスが存在します。DetailView
は1つのレコードしか扱わないため、ページネーション機能は意味のないものとなります(ページネーション機能は複数のレコードを各ページに割り付ける機能です)。したがって、DetailView
には上記で示した paginate_by
などのページネーションに関わるクラス変数は定義されておらず、これらを DetailView
のサブクラスに定義しても DetailView
のサブクラスの動作は変化しないことになります。
このように、カスタマイズを行うためにクラス変数を定義する場合、Viewのサブクラス
で定義されているクラス変数を上書きしてメソッドの動作を変化させることが目的となりますので、”Viewのサブクラス
で定義されているクラス変数” を定義しないと意味がありません。そして、この定義されているクラス変数は Viewのサブクラス
によって異なるので注意が必要です。各 Viewのサブクラス
で定義される具体的なクラス変数の種類については、Viewのサブクラス の種類 で紹介するリンク先の各ページで紹介していますので、このページを読み終えた後にでも興味のある Viewのサブクラス
のクラス変数について調べてみていただければと思います。
クラス変数のデフォルト設定
2つ目は、Viewのサブクラス
を継承するクラスを定義したとしても、そのクラスでクラス変数を定義しない限り、そのクラスは Viewのサブクラス
でのクラス変数の定義に従って動作するという点になります。もっと簡単に言えば、Viewのサブクラス
を継承するクラスで定義していないクラス変数はデフォルト設定のままとなります。そして、そのデフォルト設定は、Viewのサブクラス
で決められています。
例えば ListView
における template_name
のデフォルト設定は下記のように指定されています。
template_name
:'アプリ名/モデル名_list.html'
ここで モデル名
とはクラス変数 model
に指定したモデルクラスの名前を全て小文字にしたものとなります。アプリ名
はアプリの名前で、これも全て小文字となります。
もっと正確に言えば、この モデル名
はクラス変数 model
に指定したモデルクラスの _meta
属性で指定される名称であり、これを変更している場合は上記のようなデフォルト設定にならないので注意してください
また、クラス変数として model
ではなく query_set
を定義している場合、その queryset
からモデルクラスが特定され、そのモデルクラスの _meta
属性から上記のデフォルト設定が決定されることになります
このように、Viewのサブクラス
側でデフォルト設定が指定されているため、クラスベースビューを採用するアプリの開発方針としては2つのパターンが存在することになります。
1つ目は、用意するテンプレートファイルやコンテキストに合わせてクラス変数を定義するパターンで、2つ目は、クラス変数を定義せずにデフォルト設定に合わせたテンプレートファイルやコンテキストを用意するパターンとなります。
前者に関しては、ビュー以外に合わせてビューを作る考え方になりますし、後者に関してはビューに合わせてビュー以外を作る考え方となります。
先ほどの例で言えば、用意したテンプレートファイルのパスに合わせてクラス変数 template_name
を定義しても良いですし、template_name
のデフォルト設定のパスにテンプレートファイルを用意するのでも良いです。要は、ビューとビュー以外の部分で話があっていれば良いです。この例の場合は、ビューが利用しようとしているテンプレートファイルのパスと実際に用意されているテンプレートファイルのパスが一致していれば良いです。
ですが、どちらかというと後者の方が良いと思います。なぜなら、それによりビュー以外の部分の作りに自然と一貫性が生まれるからです。さらに、それにより、各種ファイルの役割等が分かりやすくなります。
例えば、クラスベースビューの例 で例に挙げた CommentList
の場合、model
に Comment
を指定しているため、クラス変数 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_queryset
を CommentList
で定義してオーバーライドしてやれば、CommentList
での get_queryset
実行時の動作を継承元の ListView
とは異なるものにすることができます。
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_queryset
は ListView
によって定義される get
メソッドから呼び出しされるメソッドで、データベースに発行するクエリを取得するメソッドとなります。この発行するクエリによって、データベースから取得できるインスタンスが変わることになります。そして、この ListView
のget
メソッドは CommentList
に継承されることになり、CommentList
のインスタンスから get
メソッドが実行された際には、上記でオーバーライドを行なった CommentList
の get_queryset
が呼び出しされることになります。
そのため、上記のように get_queryset
をオーバーライドすることでデータベースに発行するクエリを ListView
のものから変化させることができ、CommentList
用にカスタマイズすることが可能となります。上記の場合は、データベースから取得するインスタンスが date
フィールドに対して降順にソートされることになります。
ですが、実は上記のように get_queryset
をオーバーライドすることはあまり意味がありません。なぜなら、get_queryset
が返却する値は毎回同じだからです。毎回 “Comment
のテーブルレコードを全て取得して date
フィールドに対して降順にソートする” というクエリが返却されるだけです。つまり、この get_queryset
が返却する値は静的に決まるということになります。
こういった静的に決まる設定に関しては、メソッドのオーバーライドではなくクラス変数の定義で行うことが可能であるケースが多いです。例えば上記の例であれば、わざわざメソッドのオーバーライドを行わなくても下記のように ordering
というクラス変数を定義することで実現可能です(クラス変数 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'
ordering = '-date'
クラス変数の定義でもメソッドのオーバーライドによってもカスタマイズ可能なものに関しては、どちらかというとクラス変数の定義で実現する方が良いと思います。なぜなら、処理を記述するとバグが発生する可能性が高くなるからです。メソッドを定義すると、必ず処理の記述が必要となります。ですので、特に静的に決まる設定等のカスタマイズを行う場合はクラス変数の定義で行った方が無難だと思います。
もう少し詳細を説明すると、これらのクラス変数はアプリ起動時に Python によって解釈されることになります。したがって、アプリ起動時に既に決まっている設定等のカスタマイズはクラス変数の定義によって実現することができ、上記の通り、その方が無難です。
ですが、アプリ起動時には設定等が決まらない場合もあります。その場合、クラス変数の定義によるカスタマイズは不可となります。それに対し、メソッドのオーバーライドの場合、アプリ起動時ではなくメソッドが実行されたタイミングで設定を動的に変化させることが可能となります。したがって、アプリ起動時には設定等が決まらないカスタマイズ、言い換えれば、設定等を動的に変化させる必要のあるカスタマイズに関しては、メソッドのオーバーライドで実現する必要があります。
例えば、ユーザー登録後に、自動的にその登録されたユーザーの詳細ページに遷移させる処理について考えてみましょう!このユーザーの詳細ページの URL パターンは下記のようなものであるとしたいと思います。username
は登録されたユーザーのユーザー名とします。そして、ここで実現しようとしているのは、ユーザー登録後の下記 URL へのリダイレクトとなります。
user/<slug:username>/
こういったユーザーの登録はレコードの新規登録と考えられ、このレコードの新規登録を行うのに便利な Viewのサブクラス
が CreateView
となります。この CreateView
では、ユーザー登録フォームから送信されてきたデータの妥当性の検証を行い、検証結果が OK の場合、そのデータをレコードとしてデータベースに保存し、さらに success_url
というクラス変数によって指定される URL にリダイレクトするようになっています(”リダイレクトする” とは、ウェブアプリ側からすれば “リダイレクトレスポンスを返却すること” となります)。
ですので、CreateView
のサブクラス側で success_url
をクラス変数として定義してやれば、ユーザー登録後に、success_url
の定義に応じた URL にリダイレクトすることができるようになります。
ですが、先ほど示した URL において、username
の部分は登録されたユーザーの username
(ユーザー名) によって変化することになります。ユーザーが設定する username
は、実際にユーザー登録された後のタイミングでしか分かりません。したがって、アプリ起動時にリダイレクト先の URL を決定することは不可能です。つまり、こういったリダイレクトはクラス変数の定義では実現できません。
こんな時に利用するのがメソッドのオーバーライドによるカスタマイズになります。アプリ起動時にリダイレクト先の URL を設定することはできませんが、ユーザー登録後であれば、登録されたユーザーの username
は既に分かっているため、リダイレクト先の URL を username
に応じたものに設定することは可能です。
そして、CreateView
では下記の get_success_url
メソッドが定義されています。この get_success_url
は、レコード新規登録後のリダイレクト先の 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 返却するようにすれば、登録されたユーザーの詳細ページへのリダイレクトが実現できることになります。その具体例が下記となります。
def get_success_url(self):
return reverse('user', kwargs={'username':self.object.username})
この get_success_url
に関しては CreateView
の解説ページで詳しく説明しますが、ここで理解しておいていただきたいのは、動的にパラメーターを変化させたい場合はクラス変数の定義ではなくメソッドのオーバーライドによって実現する必要があるという点になります。クラス変数の定義でもメソッドのオーバーライドでも、どちらでもカスタマイズ可能なのであれば、前述でも説明したようにクラス変数の定義で実現するのが良いと思います。
まずは、ここで説明したことを基準に、クラス変数の定義 or メソッドのオーバーライドのどちらでカスタマイズするのかを決めるので良いと思います。ただし、そもそもクラス変数の定義ではカスタマイズ不可の場合もあるため、その場合はメソッドのオーラーライドによってカスタマイズを行う必要があります。メソッドのオーバーライドによるカスタマイズの方がカスタマイズ可能な範囲が広いです。
スポンサーリンク
Mixin
で機能を追加する
ここまでの説明のとおり、Viewのサブクラス
を継承するクラスでのクラス変数の定義とメソッドのオーバーライドによって、ウェブアプリ特有のビューを実現していくというのが、クラスベースビューの作り方の基本的な考え方になります。
ただ、これらのクラス変数の定義やメソッドのオーバーライドだけでなく、Mixin
を利用してクラスベースビューに機能を追加する方法もあるので、これについても簡単に説明しておきます。
例えば、下記ページで解説しているログイン機能をクラスベースビューで実現する際には、Mixin
を利用した機能追加が必要となります。
上記ページでも解説しているように、このログイン機能を実現する際には、単にログインを実現するだけでなく、非ログインユーザーからのアクセスを禁止することも必要となります。そしてこれは、関数ベースビューの場合、@login_required
を関数に指定することで実現可能です。
具体的には、下記のように comments_view
に @login_required
を指定すれば、非ログインユーザーからのリクエストによって comments_view
が実行されようとする際に、強制的に他のページ(例えばログインページ)にリダイレクトされるようになります。したがって、非ログインユーザーは comments_view
の実行によって表示されるページにはアクセスできないことになります。
@login_required
def comments_view(request):
# 略
ただし、この @login_required
は関数に指定可能なものであって、クラスには指定不可です。そのため、クラスベースビューの場合は @login_required
の利用は不可ということになります。したがって、クラスベースビューの場合は別の手段で非ログインユーザーからのページのアクセスを防ぐ必要があります。
この例で言えば、その別の手段とは “LoginRequiredMixin
を継承する” になります。例えば下記のように LoginRequiredMixin
を継承させれば、CommentList
はログイン中のユーザーからのリクエスト時のみ実行されるようになり、それ以外のユーザーからのリクエストの場合は別のページへリダイレクトされることになります。つまり、CommentList
によって表示されるページはログイン中のユーザーからしかアクセスできないことになります。
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
の詳細に関しては下記ページで解説を行なっています。
スポンサーリンク
DetailView
ListView
が一覧を表示する際に継承されるのに対し、DetailView
は特定のモデルクラスの1つのインスタンスの詳細を表示する際に継承される Viewのサブクラス
となります。
例えば、特定のユーザーの詳細を表示するビューなどは DetailView
を継承することによって簡単に実現することができます。
この DetailView
の詳細に関しては下記ページで解説を行なっています。
CreateView
ListView
や DetailView
がデータベースからレコードを取得して情報を表示する目的で継承されるのに対し、ここから紹介するクラスはデータベースのテーブルやレコードを変更する目的で継承される Viewのサブクラス
となります。ここでいう変更とは、新規登録・更新・削除のことを言っています。
まず紹介するのが CreateView
で、CreateView
は特定のモデルクラスのインスタンスをレコードとしてデータベースに新規登録・新規作成することを目的に継承される Viewのサブクラス
となります。
レコードの新規登録はレコードを保存するテーブルの種類によって様々な意味合いとなり、例えばユーザーの新規登録やコメントの新規投稿などを行うこともレコードの新規登録と捉えることができます。そして、これらは CreateView
を継承することで簡単に実現することが可能です。
ListView
や DetailView
とは異なり、この CreateView
ではフォームを扱うことが可能です(以降で紹介する Viewのサブクラス
も同様です)。関数ベースビューの場合、フォームを扱う際にはリクエストのメソッドが GET
の場合と POST
の場合とで処理が切り替えられるように実装する必要がありました。正直それが面倒だったのですが、CreateView
等のフォームを扱う Viewのサブクラス
を継承してビューを作成する場合は、Viewのサブクラス
内部でリクエストのメソッドに応じた処理の切り替えが行われるように作られているため、そういった処理の記述は不要となって楽にフォームを扱うことができるようになります。
この CreateView
の詳細に関しては下記ページで解説を行なっています。
UpdateView
CreateView
がレコードの新規登録を行う Viewのサブクラス
であるのに対し、UpdaateView
はレコードの更新を行う Viewのサブクラス
となります。
例えばユーザーの情報の更新ページのビューなどは UpdateView
を継承することで簡単に実現することができます。
この UpdateView
の詳細に関しては下記ページで解説を行なっています。
スポンサーリンク
DeleteView
DeleteView
はレコードの削除を行う Viewのサブクラス
となります。
例えば投稿済みのコメントを削除したり、ユーザーの退会を行うビューなどは、DeleteView
を継承することで簡単に実現することができます。
この DeleteView
の詳細に関しては下記ページで解説を行なっています。
FormView
FormView
はフォームを扱う Viewのサブクラス
となります。
ここまで紹介してきた CreateView
・UpdateView
・DeleteView
でもフォームを扱うことができますが、これらとの違いは、FormView
は基本的にデータベースのレコードの変更は行わないという点になります。もう少し違う言い方をすれば、前述の3つに関してはデータベースのレコードの変更を行うことを前提としたクラスであるのに対し、FormView
ではその前提はありません。単にフォームを扱うことに特化した Viewのサブクラス
となります。
例えば CreateView
では post
メソッドが定義されており、この post
メソッドではフォームから送信されてきたデータをレコードとしてデータベースに新規登録する処理が行われるようになっています。それに対し、FormView
でも post
メソッドが定義されていますが、この post
ではデータベースの操作は行われません。
したがって、基本的には FormView
はデータベースのレコードの変更を伴わないようなフォームを扱う際に継承することになります。
ただし、FormView
を継承するクラスで post
メソッドをオーバーライドすることでデータベースへの操作を行うようなことも可能です。つまり、FormView
は CreateView
・UpdateView
・DeleteView
よりも用途が広く、カスタマイズ次第で実現可能なビューも幅広いです。
この FormView
の詳細に関しては下記ページで解説を行なっています。
LoginView
・LogoutView
LoginView
は名前の通りログインを実現する Viewのサブクラス
となります。LogoutView
も名前の通り、ログアウトを実現する Viewのサブクラス
となります
ログインページのビューやログアウトページのビューは、それぞれ LoginView
と LogoutView
を継承するで簡単に実現することができます。
これらの LoginView
・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
問題の解決を行ないました。
今回は、上記ページの 掲示板アプリの N + 1 問題を解決する で示した views.py
の全関数をクラスベースビューに置き換えていきたいと思います。変更対象となるファイルは views.py
と urls.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つクラスベースのビューに置き換えていきます。
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
同様のことが実現できることになります。
def get_success_url(self):
return reverse('user', kwargs={'user_id':self.request.user.id})
ということで、login_view
を LoginView
を継承するクラスに置き換えた結果は下記のようになります。import
部分は省略していますが、後ほど import
部分を含めた views.py
全体のソースコードを紹介しますので、必要な import
処理に関してはそこで確認していただければと思います。
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
を継承するクラスは下記を定義することで実現することができます。
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
を利用する必要がある点がポイントとなります。
とりあえず、上記の Logout
により、ログアウトを実現するクラスベースビューが定義できたことになります。ただし、この Logout
が継承している LogoutView
はメソッドが POST
のリクエストしか受け付けないようになっており、メソッドが GET
のリクエスト受け取った時には 405
エラーをレスポンスとして返却するようになっています。
それに対し、現状の掲示板アプリでは、ログアウトはナビゲーションバーの ログアウト
リンクをクリックしたときに実施されるようになっており、このリンクのクリック時に送信されるリクエストのメソッドは GET
となっています。そのため、上記のように logout_view
を Logout
に置き換えると、ログアウト
リンクをクリックしてもログアウトが実施されず、毎回 405
エラーがレスポンスとして返却されることになります。
そのため、上記のように LogoutView
を継承するクラスを定義すると同時に、ログアウト
リンクがクリックされた時に送信されるリクエストのメソッドが GET
ではなく POST
になるように変更が必要となります。具体的には、このリンクを出力しているのはテンプレートファイルの base.html
ですので、base.html
の変更が必要となります。この base.html
の変更に関しては、後述の テンプレートファイルの変更 で解説します。
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
と同等の動作を実現することができます。その例が下記となります。
class Index(RedirectView):
url = reverse_lazy('comments')
users_view
から ListView
のサブクラスへの置き換え
次は users_view
のクラスベースビューへの置き換えを行なっていきます。
users_view
は User
(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_by
と page_kwarg
のクラス変数の定義が必要となります。
paginate_by = 3
page_kwarg = 'p'
テンプレートファイルの指定
また、users_view
ではテンプレートファイル 'forum/users.html'
を利用するようになっているため、クラスベースのビューからも、このファイルを利用するようにカスタマイズを行う必要があります。
これは、利用するテンプレートファイルに合わせてクラス変数 template_name
の定義を行えば良いだけになります。users_view
は tempalates
フォルダからの相対パスで 'forum/users.html'
の位置にあるファイルをテンプレートファイルとして利用しているわけですから、下記のように template_name
を定義してやれば良いことになります。
template_name = 'forum/users.html'
非ログインユーザーのアクセス禁止
また、非ログインユーザーのアクセスからのアクセスの禁止は、クラスベースビューの場合は @login_required
の指定ではなく LoginRequiredMixin
の継承によって実現することができます。このように、クラス変数やメソッドのオーバーライドだけでなく、ミックスインの継承によってもビューをカスタマイズすることができることは覚えておくと良いと思います。
ちなみに、LoginRequiredMixin
によってリダイレクトが行われる場合、リダイレクト先の URL は、settings.py
での LOGIN_URL
の定義によって設定可能です。これは、@login_required
と同様の設定方法となりますので、詳細に関しては下記ページをご参照ください。
ということで、users_view
と同等のクラスベースビューは、下記のような 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'
は下記のようなテンプレートファイルとなっています。ポイントは、このテンプレートファイルから参照している変数になります。
{% 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_obj
は Page
クラスのインスタンスであることを期待しており、この page_obj
のデータ属性を出力することで前後のページ等へのリンクを設定し、さらに page_obj
からインスタンス user
を取得し、この user
のデータ属性を出力することで各ユーザーの情報の表示を実現するようになっています。
また、テンプレートファイルから HTML を生成する際には、テンプレートファイルから参照する変数がコンテキストにセットされている必要があります。forum/users.html
の場合は、'page_obj'
という変数名で変数を参照しており、この変数は Page
のインスタンスである必要があるため、コンテキストの 'page_obj'
キーの値として Page
のインスタンスがセットされている必要があります。
実際、元々このテンプレートファイルを利用していた 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_view
は User
(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
を継承しているのは、前述の通り、非ログインユーザーからのアクセスを禁止するためです。
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つのインスタンスのみを取得することができるようになっています。
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
として受け取ることを前提とした作りになっています。つまり、urlpatterns
と DetailView
とで話が合っていないことになります。この場合、DetailView
が動作した際に例外が発生することになります。
ただし、インスタンスを特定するための整数を受け取る引数の引数名は pk
から変更可能です。この引数名を変更するために定義するクラス変数が pk_url_kwarg
で、このクラス変数を定義することで、この引数名を pk
から変更することが可能です。UserDetail
では pk_url_kwarg = 'user_id'
を定義しているため、この引数名が user_id
となり、URL で <int:user_id>
部分に指定された整数を UserDetail
が受け取ることができるようになります。
そして、UserDetail
は、受け取った整数とプライマリーキーが一致するインスタンスを User
のテーブルから取得することになります。
解説を読んで気づかれた方もおられるかもしれませんが、わざわざ pk_url_kwarg
を定義しなくても、urlpatterns
の中で実行している path
関数の第1引数における <int:user_id>
部分を <int:pk>
に変更するのでもオーケーです。大事なのは、ビューと urls.py
とで話が合うように、それぞれのファイルを実装することです。
あとは、template_name
を user_view
から利用しているテンプレートファイルに合わせて定義してやれば、特定のユーザーの詳細情報を表示するページは完成することになります。
Comment
の一覧の追加
次は、取得した User
のインスタンスと関連付けられている Comment
のインスタンスの一覧の表示を行うようにしていきたいと思います。最初に結論を言えば、下記のように UserDetail
に get_context_data
メソッドの定義を追加することで、この Comment
のインスタンスの一覧の表示を実現することが可能です。
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_data
は DetailView
の持つメソッドで、コンテキストを生成して返却するメソッドとなっています。DetailView
の get_context_data
では可変個のキーワード引数を受け取れるようになっており、下記のように各キーにデータをセットしたコンテキストを生成するようになっています。
'object'
:model
のインスタンス'modelに指定したモデルクラスの名前(小文字)'
:model
のインスタンス'キーワード引数 1 の引数名'
: キーワード引数 1 の値'キーワード引数 2 の引数名'
: キーワード引数 2 の値- (以下、同様にして指定されたキーワード引数に応じてデータがセットされる)
ただし、この get_context_data
は DetailView
で定義される get
メソッドから実行されるようになっており、その際にはキーワード引数が指定されないようになっています。したがって、コンテキストにセットされるデータは、上記における 1. と 2. の model
のインスタンスのみとなります。なので、UserDetail
の場合は User
のインスタンスしかテンプレートファイルから参照できないことになります。これだとテンプレートファイルからは Comment
のインスタンスが参照できません…。
ですが、前述の通り get_context_data
自体は可変個のキーワード引数を受け取れるようになっており、そこで指定した引数に応じて、上記の 3. 以降のようにコンテキストにデータがセットされるようになっています。そのため、get_context_data
をオーバーライドし、コンテキストにセットしたいデータを用意してキーワード引数を指定した状態でスーパークラス(DetailView
)のメソッドを実行するようにすれば、任意のデータをコンテキストにセットできることになります。
それを行なっているのが上記の UserDetail
の get_context_data
で、UserDetail
の get_context_data
では、user=self.object
を満たす Comment
のインスタンスの集合を取得し、そのインスタンスの集合からページネーションを行い、クエリパラメーター 'p'
で取得されるページ番号の Page
のインスタンス page_obj
を生成するようになっています(self.object
には User
のインスタンスがセットされています)。
さらに、その生成した page_obj
をキーワード引数に指定する形でスーパークラス、つまり DetailView
の get_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
の定義によってコンテキストへの要素の追加が可能です。
ただし、今回はコンテキストにセットする Page
のインスタンスを動的に変化させる必要があったため、もっと具体的に言えばクエリパラメーター p
で指定されるページ番号に応じて Page
のインスタンスを動的に変化させる必要があったため、get_context_data
のオーバーライドによってコンテキストに追加する要素を動的に設定するようにしています。
comments_view
から ListView
のサブクラスへの置き換え
comments_view
のクラスベースビューへの置き換えに関しては users_view から ListView のサブクラスへの置き換え とほぼ同様であるため、解説は省略します。
comments_view
と同等のクラスベースビューは下記の 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
によって実現することができます。
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
を定義し、このクラス変数で利用するモデルフォームクラスを指定する必要があります。モデルフォームに関しては下記ページで解説しているため、詳しくは下記ページを参照してください。
register_view
ではモデルフォームクラスとして RegisterForm
を利用しているため、register_view
の代わりとする CreateView
のサブクラスでは form_class = RegisterForm
を定義してやれば良いことになります。RegisterForm
は User
(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
メソッドのオーバーライドにより実現することができます。CreateView
の form_valid
は、フォームから送信されてきたデータに対する妥当性の検証結果が OK の場合に実行されるメソッドであり、レコードの新規保存、および、クラス変数 success_url
で指定される URL or get_success_url
メソッドから返却される URL へのリダイレクトのレスポンスの返却のみが行われるようになっています。
なので、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
のオーバーライドによって実現しています。
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
のインスタンスがデータベースに新規登録されるような動作を実現することができます。
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
によって実現することができます。
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
全体を示しておきます。
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
に置き換えると ログアウト
リンクのクリック時に必ずエラーが発生し、ログアウトを実施することができなくなります。
この ログアウト
リンクのクリック時に、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 %}
を記述する必要があるという点に注意してください。
上記のように変更することで、ナビゲーションバーが下の図のように変化し、ログアウト
ボタンのクリック時にはメソッドが 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
全体を下記に記しておきます。
<!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
メソッド実行結果に置き換えてやれば良いです。
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引数で指定したクラスが動作することになります。
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のサブクラス
ごとに解説ページを設けて説明していますので、興味があれば下記ページも読んでみていただければと思います!