お知らせ
(ちょっと古いですが)この記事の内容を使ってUdemyで入門講座作っています。
興味ある方はこちらから
お知らせここまで
間違い、勘違いなどありましたら優しくご指摘ください。
- Django 2.0 以上
で動作すると思います。
なぜこの記事を書くのか
Djangoには強力なテンプレート言語があります。
テンプレートにデータを供給するのはViewです。
そして、それを手短にコーディングする方法も用意されています。
それが「汎用ビュー(Generic View)」です。
はるか昔は関数ベースの汎用ビューを使っていたようですが、現在は主にクラスベースのビューを使います。
現在のDjangoの和訳ドキュメントは1.4が最新ですが、クラスベース汎用ビューのドキュメントはちゃんとあります。
しかし、思ったよりもクラスベース汎用ビューを解説している記事が少なかったため、書き記そうと思いました。
この記事の内容
- Djangoでクラスベース汎用ビューを使う方法
汎用ビューとは何か
viewとはRailsでいうところのコントローラーに当たります。DjangoはMTV(Model, Template, View)という考え方なのでviewと呼んでいるようです。
データをテンプレートに供給する役目を負っていますが、ウェブサイトのほとんどは
- 特定の条件下のリスト
- DBから記事を絞り込んで表示する
- 単一の記事を表示する
という機能を持っています。何度も同じようなコードを書かなくても済むようにあらかじめDjangoに処理を定義してあります。
それらを「汎用ビュー」と呼び、関数で提供されていれば「関数ベース汎用ビュー」、クラスで提供されていれば「クラスベース汎用ビュー」と呼ぶようです。
汎用ビューでは、モデルやフォームなどに応じて適切なデフォルト値を設定しておいてくれます。
したがって、プログラマが記述する量がとても少なくなるという利点があるのです。
この記事では、「クラスベース汎用ビュー」について書き留めています。
クラスベース汎用ビューはそれぞれの同じような処理をクラスで定義しています。
私たちは、そのクラスを継承し、必要であればクラス変数を変更し、またメソッドをオーバーライドして自身の処理を挿入したりできます。
クラスベース汎用ビューの使い方
最初の一歩
最初の一歩ですので、まずはどのように記述するのかにフォーカスを当ててみます。
汎用ビューは主にviews.py
などで使用されます。
また、urls.py
でas_view()
という関数でURLを紐つけます。
サンプルを書いてみましょう。
django-admin
などでプロジェクトを作成し、manager.py startapp foo
としてアプリケーションを一つ作成したものとします。
さて、書いていきます。
from django.views.generic import TemplateView
class SampleTemplateView(TemplateView):
template_name = "index.html"
これで使用可能です。
記述量がとても少ないことがわかります。
ここでは、TemplateView
が汎用ビューです。
django.views.generic
などに、それぞれの用途に応じたビューが準備されていますから、import
して継承します。
URLは、
from django.urls import path
from foo.views import SampleTemplateView
urlpatterns = [
path('sample', SampleTemplateView.as_view()),
]
とすれば、/sample
にアクセス際にSampleTemplateView
がよしなに処理してくれます。
このように、views.py
に書き込むコードの量を激減させることができます。
TemplateView
はその名の通りテンプレートを使って描画させることを目的としています。
TemplateView
ではなく他の汎用ビューを使えば、DBからデータを取得するコードを書くこと無く一覧表示ができたり、特定の条件で絞り込んだあとのデータを出力することができます。
テンプレートに自分で設定した変数を渡したい
はじめの一歩では、ただテンプレートを表示することしかできませんでした。
しかし、普通はDBから読み込んだデータを表示したくなるでしょう。
そんな時は汎用ビューで定義されているメソッドをオーバーライドします。
私がよくオーバーライドしているメソッドはget_context_data
です。
以下のようにして使います。
from django.views.generic import TemplateView
class SampleTemplate(TemplateView):
template_name = "index.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # はじめに継承元のメソッドを呼び出す
context["foo"] = "bar"
return context
のように使います。
上記のコードの場合、テンプレートでfoo
という変数を使えるようにしています。
テンプレートで{{ foo }}
とすればbar
となります。
テンプレートのファイル名をindex.html
などとすると
<div class="sample">{{ foo }}</div>
とすれば、
<div class="sample">bar</div>
となるということです。
つまり、このメソッド内に自身が挿入したい処理を記述し、get_context_data
の戻り値に入れておけば、テンプレートで使用できる、ということになります。
この章まとめ
汎用ビューにはこのようにメソッドをオーバーライドして自分が必要な処理を追加していきます。
これが基本的な使い方です。
一覧表示するためのListView
や、個別ページを作成するためのDetailView
など便利な汎用ビューがたくさんありますので、紹介して行きます。
クラスベース汎用ビューの種類(一部)
注:ここに紹介しているものが全てではありません
を見ると、RedirectView
やFormView
などもっとたくさんの汎用ビューが定義されています。
TemplateView
TemplateView
はその名の通り、テンプレートを指定して何かを表示する汎用ビューです。
したがって、テンプレート名はかならず指定しなければなりません。
また、get_context_data
を呼び出すので、必要なデータはオーバーライドして設定します。
もちろん、汎用ビューの管理外ですので、自身で件数やフィルタリングを行う必要があります。
from django.views.generic import TemplateView
class MyTemplateView(TemplateView):
template_name = "index.html" # 必須
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# いろいろやる
return context
筆者は主にサイトのトップページなどで使用しています。
ListView
ListView
はその名の通り、一覧ページなどを作る時に使用します。
単純に対象のモデルのリストを作成すると、
from django.views.generic import ListView
from foo.model import MyModel
class MyListView(ListView):
model = MyModel
のように定義します。
model = MyModel
はリストを作成するモデルです。
テンプレートではこのようになるでしょう。
<ul>
{% for item in object_list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
DBにたくさんのデータが入っていても、ListView
はデフォルトで10件ずつリストを分割します(コメントでの指摘により変更)全件取得しますが、後述する変数で一ページに出力する件数を制御できます。
分割する件数はpaginate_by
という変数です。
つまり、5件ずつ分割したいのなら
class MyListView(ListView):
model = MyModel
paginate_by = 5
と定義します。
これは、100件データがあったら、一ページに5件だけ表示させる、という設定です。
また、時にはあらかじめ取得するデータのソート順を変えたり、フィルタリングして制御したい時もあるでしょう。
その時はget_queryset
メソッドをオーバーライドします。
class MyListView(ListView):
model = MyModel
paginate_by = 5
def get_queryset(self):
return MyModel.objects.filter(some_column=foo)
のようにします。
そうすれば、get_queryset
で定義されたクエリを発行し、データをobject_list
に格納します。
もし、一覧に抽出する件数を制限したいときはここで指定します。
指定は普通のスライスです。
def get_queryset(self, queryset=None):
return MyModel.objects.all()[:10]
これでたとえデータが100件あっても10件しか抽出されなくなります。
この書き方は、もう少し短くできます。
class MyListView(ListView):
model = MyModel
paginate_by = 5
queryset = MyModel.objects.filter(some_column=foo)
どちらを使っても結果は変わりません。
動的にクエリを発行したいのなら前者を、常に同じ結果を求めるのなら後者を使えば良いと思います。
もちろん、get_context_data
メソッドをオーバーライドすれば、他のテーブルからのデータをテンプレートに渡すことができます。
class MyListView(ListView):
model = MyModel
paginate_by = 5
def get_queryset(self):
return MyModel.objects.filter(some_column=foo)
def get_context_data(self, **kwargs):
context = super().get_context_data(**data)
context["bar"] = OtherModel.objects.all() # 他のモデルからデータを取得
return context
ただ、この場合、OtherModel
は汎用ビューの制御下にはありませんので、context["bar"]
にはOtherModel
の全件数が格納されます。
DetailView
DetailView
はその名の通り、個別詳細ページを対象とした汎用ビューです。
レコード一列を対象としてデータを取得します。
from django.views.generic import DetailView
from mymodel.model import MyModel
class MyDetailView(DetailView):
model = MyModel
URLは
urlpatterns = [
path('<int:pk>', MyDetailView.as_view()),
]
とします。
pk
はプライマリーキーのことですが、DetailView
はこの名前を使ってレコードを特定します。
変更することもできますが、必要がなければこのままでよいでしょう。
CreateView・UpdateView
CreateView
は新たにレコードを追加するフォームを提供するビューです。
UpdateView
はお察しの通り、すでにあるデータを更新するフォームを提供するビューです。
from django.views.generic.edit import CreateView
class MyCreateView(CreateView):
model = MyModel
fields = ("name", ) # リストもしくはタプル
このように定義します。他の汎用ビューとさほど変わりません。
ただ、CreateView
やUpdateView
にはfields
という変数を定義する必要があります。
これは、フォームからデータが入力されても、fields
に定義したフィールド以外は無視するというものです。
RailsのStrongParametersのようなものでしょうか。
CreateView
やUpdateView
が他の汎用ビューと異なる点は、「フォームを作成」するということです。
つまり、object
やobject_list
ではなく、form
という変数を生成します。
form
変数にはフォームを作成するためのデータが格納されていて、フォームを簡単に作成できます。
form
変数を使ったテンプレートはこうなります。
<form method="post">
{% csrf_token %}
<table>
{{ form }}
</table>
<input type="submit">
</form>
HTMLタグの<form></form>
は自分で書かなければなりませんが、フォームフィールドは生成してくれます。
{{ form }}
はデフォルトでテーブルを使ったフィールドを出力します。
<p></p>
タグを使ったフィールドにしたいときは{{ form.as_p }}
とます。
{% csrf_token %}
はCSRF対策です。DjangoでPOSTフォームなどを作った場合は必須です。
また、ModelForm
などのフォームオブジェクトを利用することもできます。
このようにして、簡単に既存のフォームクラスを利用することができます。
from django import forms
from myapp.model import MyModel
class MyModelForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ("name", )
from forms import MyModelForm
class MyCreateView(CreateView):
form_class = MyModelForm
また、これらのフォームで重要な機能としてバリデーションがあります。
せっかくフォームを送信しても、バリデーションでエラーになり、追加・更新されない場合、ユーザーに通知しなければ混乱するかもしれません。
そこで、これらの汎用ビューにはform_valid
とform_invalid
というメソッドが定義されていて、バリデーションが通ったか否かで自身が必要な処理を実行できます。
例えば、送信したフォームが上手く保存されたかどうかをユーザーに通知することは良くあります。
そんな時Djangoではメッセージフレームワークを使います。
from django.views.generic.edit import CreateView
from django.contrib import messages # メッセージフレームワーク
from myapp.models import MyModel
class MyCreateView(CreateView):
model = MyModel
def form_valid(self, form):
''' バリデーションを通った時 '''
messages.success(self.request, "保存しました")
return super().form_valid(form)
def form_invalid(self, form):
''' バリデーションに失敗した時 '''
message.warning(self.request, "保存できませんでした")
return super().form_invalid(form)
ここでは、バリデーションに成功した時に呼ばれるform_valid
メソッドと、バリデーションに失敗した時に呼ばれるform_invalid
に独自のメッセージを追加しています。
こうすることによって独自の処理を追加できるので、例えば上記の例のほか、エラーが発生した時にメールで管理者に通知するなどの処理を挿入することができます。
独自の処理を終えたら、継承元のメソッドを呼び出して、戻り値を返します。
ちなみに、メッセージを表示するには以下のようにします。
ドキュメントの内容そのままですが。
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
DeleteView
DeleteView
はその名の通り、レコードを削除します。
テンプレートは(AppName)/(AppName)_confirm_delete.html
です。
from django.views.generic.edit import DeleteView
from myapp.models import MyModel
class MyDeleteView(DeleteView):
model = MyModel
この場合のテンプレートは「本当に削除してもよいですか?」という確認のフォームになるでしょう。
もしかしたら本当に削除するのではなく、削除フラグをONにしたいだけの時もあると思います。
そんな時はdelete
メソッドをオーバーライドするのが良いと思います。
ただ、筆者は素直に削除してしまうので試したことはないです。
共通項目
クラスベース汎用ビューは前述したそれぞれの処理はもちろん、テンプレートのファイル名、変数名についても自由にオーバーライドできます。
また、その方法も共通していますので、いくつか紹介します。
普段はデフォルト値を使い、必要なときだけオーバーライドするのが良いと思います。
デフォルト変数
すでにお気づきかと思いますが、汎用ビューにはある程度の規則性があります。
get_context_data
をオーバーライドし、必要な処理を行い、変数にセットすることは初めの一歩で紹介しました。
テンプレートにデータを渡したいときはほとんどこれで事足りると思います。
ではその反対に、汎用ビューが自動的にデータを取得するとき、どんな変数にデータにどんなデータがセットされるのでしょうか。
クラスベース汎用ビューでは、その用途(継承するクラス)に応じて自動的にDBからデータを取得します。
どのテーブルからデータを取得するのかはmodel = MyModel
のように設定します。
class MyListView(ListView):
model = MyModel # MyModelのリストを自動的に取得する
このようにすることで、MyModel
からデータを取得します。
その結果などは決まった名称の変数にセットされることになっています。
個別ページなどのレコード一つだけならobject
。
複数であればobject_list
。
テンプレートでは以下のようになるでしょう。
個別ページなど
{{ object }}
複数であれば
{% for item in object_list %}
{{ item }}
{% endfor %}
しかし、これでは何を指しているのかテンプレートを見てもわかりません。
いまはobject_list
という変数名ですが、もっとわかりやすい名前にしたいこともあると思います。
Djangoは自動でモデル名+_list
という変数を用意してくれています。
今回のモデルはMyModelですので、mymodel_list
という変数がテンプレートで使えます。
つまり、object_list
とmymodel_list
がどちらも使える、ということです。
いや、自動じゃなくて自分でもっとおしゃれな名前をつけたいんだ、というアナタ。
そんな時は、context_object_name
に変数名を代入します。
class MyBookList(ListView):
model = Books
context_object_name = "my_books" # この行で変数名を指定
このようにすると、テンプレートでは
<ul>
{% for book in my_books %}
<li>{{ book.name }}</li>
{% endfor %}
</ul>
のようにして呼び出せます。
変数名が何を指すのかはっきりしたので、テンプレートを読んだだけで変数が何を示しているかわかりやすくなります。
テンプレートとして使うファイルの指定
viewsはコントローラーです。
したがって、適切なテンプレートにデータを渡します。
テンプレートは明示的に指定することもできますが、デフォルトのテンプレート名を持っています。
それは、
(ModelName)/(ModelName)_(ViewName).html
というものです。
それらは、
-
AppName
-
manage.py startapp
などで作成したアプリケーションの名前
-
-
ViewName
-
ListView
であればlist
、DetailView
であればdetail
など、固有の名称
-
という事になっています。
つまり、myapp
でListView
を使うと、デフォルトでmyapp/myapp_list.html
を探します。
これを変更したいときは、
class MyListView(ListView):
model = MyModel
template_name = "my_list_view.html" # この行でテンプレート指定
のようにします。
余談
汎用ビューを使っていると、同じ変数をすべてのビューで定義したくなります。
たとえば、「サイト名」です。
すべてのビューでそれを定義するのはイマイチなので、共通のクラスを作って継承しようかなぁと思ったりします。
from django.views.generic import ListView
from myapp.models import MyModel
# Mixinなどを継承せず、objectを継承してください。
class MyCommonView(object):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["SITE_NAME"] = "SITE-NAME"
return context
class MyListView(ListView, MyCommonView):
model = MyModel
これで、サイト名を変更したいときは一箇所変更するだけで済みます・・・か?
それも一つの方法でしょうが、私は素直にContextProcessorを使ったほうがいいと思います。
あとがき
疲れました。
いくらかでもDjangoを使う人の参考になればと思います。
一応、
- Django 2.2.8
で動作を確認していますが、おかしな所がありましたら優しく教えて下さい。
お知らせ
(ちょっと古いですが)この記事の内容を使ってUdemyで入門講座作っています。
興味ある方はこちらから
お知らせここまで