【Django】DetailViewの使い方(クラスベースビューでの詳細ページの実現)

DjangoのDetailViewの解説ページアイキャッチ

このページでは、Django フレームワークで定義される DetailView の使い方について説明していきます。

この DetailViewView というクラスのサブクラスであり、ビューをクラスベースで作成する際に利用するクラスとなります。この View のサブクラス を継承し、さらにクラス変数を定義したりメソッドをオーバーライドすることで、あなたが開発したいアプリに応じたビューを作成することが可能となります。

この辺りのクラスベースビューやクラスベースビューの作り方については下記ページで解説していますので、詳しくはこちらをご参照ください。

クラスベースビューの解説ページアイキャッチ【Django入門14】クラスベースビューの基本

このページでは、前述の通り View のサブクラス の1つである DetailView に焦点を当てて解説を行なっていきます。

また、View のサブクラス には ListView というクラスも存在しており、下記ページで ListView を継承したクラスベースビューの作り方について詳しく解説しています。ListViewView のサブクラス の中でも最も基本的なクラスの1つであり、この ListView の解説ページを読んでいただければ大体 View のサブクラス を継承したクラスの使い方については理解していただけると思います。

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

DetailView に関する解説も、適宜上記の ListView の解説ページを参照しながら解説を行なっていきますので、事前に上記ページを読んでおいていただくことをオススメします。

DetailView

DetailView特定のモデルクラスにおける1つのインスタンスの詳細を表示するページを実現する View のサブクラス となります。

DetailViewdjango.views.generic から import して利用します。

DetailViewのimport
from django.views.generic import DetailView

例えば SNS アプリでのユーザーの詳細ページ、ショッピングアプリでの商品の紹介ページといった、特定のモデルクラスにおける1つのインスタンスの詳細を表示するページは DetailView を継承することで簡単に作成することが可能です。

DetailViewの説明図

基本的には、DetailView が対応しているのは表示だけであり、HTTP のリクエストメソッドとしては GET のみに対応しています。そして、リクエストを受け取った際には、この DetailViewget メソッドが実行されるようになっています。

同様に、下記ページで解説している ListView も特定のモデルクラスのインスタンスを表示するのが得意なクラスとなります。が、ListView の場合は複数のインスタンスの一覧を表示するビューを実現するために継承するのに対し、DetailView の場合は1つのインスタンスの詳細を表示するビューを実現するために継承するクラスとなります。

クラスベースビューの解説ページアイキャッチ【Django入門14】クラスベースビューの基本

そのため、ListView の場合は表示対象となるモデルクラス or 表示対象となるインスタンスを取得するためのクエリの指定を行うことが重要でしたが、DetailView の場合は、それらに加え、表示対象となる1つのインスタンスを特定するための情報が必要となります。

この辺りが ListViewDetailView の大きな違いとなるのですが、定義されているクラス変数やメソッドに共通なものも多いため、共通している部分は ListView の解説ページへのリンクを示すだけで、本ページでの詳細な解説は省略させていただこうと思います。

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

次は DetailView でクラスベースビューを作成する手順を説明します。

DetailView でクラスベースビューを作成する際も、大まかに言えば ListView でのクラスベースビューの作り方 で説明した ListView でクラスベースビューを作成する場合と同様となります。

要は、views.py に DetailView を継承するクラスを定義し、views.py に定義したクラスへクラス変数やメソッドを定義することで DetailView で定義されているクラス変数の上書きメソッドのオーバーライドを行なっていきます。これにより、DetailView の特徴を活かしながら自身のウェブアプリに応じたビューにカスタマイズしていくことが可能となります。

クラスベースビューの作り方の説明図

もう少し具体的に言えば、前述の通り、DetailView ではリクエストを受けとった際に get メソッドが実行されるようになっており、DetailView のクラス変数や他のメソッドは、この get メソッドから利用されるようになっています。

DetailViewのgetメソッドが実行される様子

したがって、DetailView を継承するクラス側でクラス変数やメソッドを上書き・オーバーライドしてやれば、この get メソッドの動作を変化させることができます。

また、DetailView の場合は ListView の時同様に最低限 model or queryset を定義する必要があります。これらの定義により、インスタンスの取得先のテーブルやインスタンスを取得する際に発行するクエリを指定することができます。

従って、DetailView を継承してクラスベースビューを作成する場合も、最低限下記のようにクラスを定義してやれば良いことになります。model の代わりに queryset を定義するのでも良いです。

DetailViewの継承
from django.views.generic import DetailView
from .models import モデルクラス

class クラス名(DetailView):
    model = モデルクラス

スポンサーリンク

インスタンスを1つに特定する

ですが、DetailView の場合、ListView とは異なり、取得するインスタンスを1つに特定することが必要となります。そのため、DetailView は URL でプライマリーキー (pk) もしくはスラッグ (slug) が指定されることを期待した作りとなっています。まずは、URL でプライマリーキーが指定されることを前提に解説していきます。スラッグについては後述で解説します。

URL でプライマリーキーが指定された場合、DetailViewmodel で指定したモデルクラスの全インスタンス or queryset で指定されたクエリで取得される全インスタンスの中から、プライマリーキーのフィールドの値が URL で指定されたプライマリーキーと一致するインタンスのみを取得するようになっています。

プライマリーキーは各インスタンスを識別するための情報ですので、プライマリーキーが指定されることで必ず1つのインスタンスのみが取得されるようになります。また、特に設定を変更していない場合は各インスタンスの id フィールドがプライマリーキーとなっています。また、プライマリーキーとなっているフィールドは pk データ属性からも参照可能となっています。

idがプライマリーキーを示す様子

DetailView が上記のような作りとなっているため、DetailView を継承してビューを作成する場合、基本的には下記の形式の URL に対するリクエストを受け取れるようにしておく必要があります。

/パス/プライマリーキー/

そして、これは urls.py (アプリ側) の urlpatterns を下記のように定義することで実現できます。

pkの指定
from django.urls import path
from . import views

urlpatterns = [
    path('パス/<int:pk>/', views.クラス名.as_view()),
]

つまり、DetailView を継承するクラスを動作させるためには、基本的には上記のように path 関数の第1引数で指定する URL パターンに <int:pk> が含まれるようにしておく必要があります。そして、この <int:pk> 部分で指定された整数がプライマリーキーとして扱われます。実は、この pkの部分はクラス変数の定義により変更することも可能です。これに関しては後述で解説します。また、先ほども少し触れたように、プライマリーキーではなくスラッグを指定して表示対象のインスタンスを1つに特定するようなことも可能です。これに関しても後述で解説します。

このように、DetailView を継承する場合は表示対象のインスタンスを1つに特定することが重要であり、DetailView が期待する情報が URL に指定可能となるように urls.py を作る必要があります。この辺りに関しては ListView にはない DetailView ならではの特徴であると言っていいと思います。そして、こういった特徴があるため、DetailView には ListView に定義されていないクラス変数が存在し、この辺りの設定をクラス変数の定義で変更可能となっています。

特に、この辺りに注目しながら、DetailView のクラス変数DetailView に定義されているクラス変数を紹介していきたいと思います。

また、DetailView においてもページの表示を実現するためにはテンプレートファイルが必要となります。この際に利用されるテンプレートファイルは ListView 同様に template_name で指定したパスのテンプレートファイル or DetailView でデフォルトで設定されているパスのテンプレートファイルとなります。

したがって、これらのパスには “1つのインスタンスの詳細情報を表示するためのテンプレートファイル” を設置しておく必要があります。ListView の場合と違って複数のインスタンスの情報を表示するテンプレートファイルではなく、1つのインスタンスのみの情報を表示するテンプレートファイルを用意しておく必要がある点に注意してください。

DetailView のクラス変数

では、次は DetailView で定義されているクラス変数の紹介を行なっていきます。これらのクラス変数を DetailView を継承するクラスで定義し直して上書きすることでクラスの動作のカスタマイズを行うことが可能となります。

DetailView の場合、GET メソッドのリクエストを受け取った際に DetailViewget メソッドが実行されることになるため、この get メソッドの動作を変化させることを目的にクラス変数の定義を行なっていくことになります。

DetailView のクラス変数の一覧

その DetailView で定義されるクラス変数には下記のようなものが存在します。

特に extra_contexttemplate_engineresponse_classcontent_type に関しては ListView のクラス変数の役割が全く同じなので ListView の解説ページを参照していただければと思います(これら4つの各リンクには ListView の解説ページでの説明部分へのリンクを設定しています)。

また、これらが DetailView で定義されているクラス変数の全てというわけではないので注意してください。カスタマイズに利用する機会の多そうなもののみを紹介しています。全てのクラス変数を知りたい場合は、実際に DetailView の定義をソースコードで確認していただくのが一番早いと思います。

また、ここからは、アプリの models.py で下記のような Comment が定義されていることを前提に解説を行なっていきます。

models.py
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=256)
    date = models.DateField(auto_now_add=True)
    slug = models.SlugField(max_length=256, null=True, unique=True)

slug は URL でスラッグを指定して1つのインスタンスに特定するためのフィールドとなります。これに関しては後述の slug_url_kwarg で説明します。

では、先ほど挙げたクラス変数の役割や、これらを定義することで実現できることについて、1つ1つ説明していきます。

スポンサーリンク

model

前述の通り、DetailView は特定のモデルクラスの1つのインスタンスを取得して表示するクラスになります。このインスタンスの取得先のモデルクラスを指定するクラス変数が model となります。

考え方は基本的には ListView の model と同様になります。

例えば、Comment から1つのインスタンスを取得して表示したいのであれば、ListView を継承したクラスに対して下記のように model を定義してやれば良いです。

modelの定義
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment

これにより、プライマリーキーが “URL で指定された pk” と一致するインスタンスが取得されるようになります。そして、そのインスタンスの情報をテンプレートファイルに埋め込んでページとして表示することができるようになります。

queryset

model のみを定義した場合、テーブル全体の中からプライマリーキーが URL で指定された pk と一致するインスタンスが検索されて取得されることになります。それに対し、queryset を定義した場合は、テーブル全体ではなく queryset に指定されたクエリによって取得されるインスタンスの集合の中からプライマリーキーが URL で指定された pk と一致するインスタンスが検索されて取得されるようになります。

queryset の定義の仕方やクエリの指定の仕方に関しては ListView の queryset と基本的な考え方は同じなので、ここでの詳しい説明は省略します。

pk_url_kwarg

さて、ここまで URL ではプライマリーキーが指定され、そのプライマリーキーから1つのインスタンスが特定されることを前提に説明をしてきました。そして、その URL で指定されたプライマリーキーは pk という変数名で扱う必要があると説明してきました。

前述の通り、この場合、urls.py (アプリ側) では下記のように変数名を pk として定義する必要があります。

pkの指定
from django.urls import path
from . import views

urlpatterns = [
    path('パス/<int:pk>/', views.CommentDetail.as_view()),
]

で、この変数名が pk であることが前提で DetailView が動作するようになっているため、下記のように異なる変数名でプライマリーキーを受け取るように urls.py を変更してしまうと、DetailView を継承したビューが動作する際に例外が発生することになります。

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

urlpatterns = [
    path('パス/<int:comment_id>/', views.CommentDetail.as_view()),
]

発生する例外は下記のようなものになります。

Generic detail view CommentDetail must be called with either an object pk or a slug in the URLconf.

このように、DetailView ではプライマリーキーが pk という変数名で扱われることを前提に動作しています。これ以外にも、DetailView は様々な前提のもとに動作するようになっています。そのため、DetailView を継承したビューを扱う際には、その前提に合わせてアプリを作成する or その前提を変更する必要があります。

そして、この前提はクラス変数の定義で変更することが可能で、上記の URL で指定されるパラメーターの変数名に関しては pk_url_kwarg によって変更可能です。この pk_url_kwarg は URL で指定されるプライマリーキーの変数名を定義するクラス変数であり、デフォルトでは 'pk' となっています。従って、pk_url_kwarg を定義しなかった場合、プライマリーキーの変数名は pk である必要があります。

ですが、pk_url_kwarg を定義してやれば、プライマリーキーの変数名は pk から変更することが可能となります。例えば下記のように pk_url_kwarg を定義した場合、CommentDetail はプライマリーキーの変数名が comment_id であることを前提に動作するようになります。

pk_url_kwargの定義
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment
    pk_url_kwarg = 'comment_id'

したがって、urls.py においては、下記のようにプライマリーキーを comment_id という変数名で指定できるようにしておく必要があります。

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

urlpatterns = [
    path('パス/<int:comment_id>/', views.CommentDetail.as_view()),
]

結局、DetailView を継承するクラスで受け取るプライマリーキーの変数名と urls.py で定義するプライマリーキーの変数名が一致していれば問題なく動作しますし、一致していなければ例外が発生するという単純な話です。これは実は関数ベースビューの場合も同様で、関数ベースビューの場合も urls.py で定義する変数名と、関数がそれを受け取る引数名が一致している必要があります。

関数ベースビューとURLで指定されるパラメータの変数名を同じにする様子

ただ、関数ベースビューの場合は、その変数名も引数名も開発者自身が必ず定義する必要があるため、単に両方を同じものに合わせて定義してやれば良いだけになります。ですが、クラスベースビューの場合は、先ほどの例の pk のように、View のサブクラス で受け取ることを前提としている変数名などが決まっているため、それに合わせて他の部分を作成する必要がある or その前提を変更してやる必要があるため、View のサブクラス の作りをある程度理解しておく必要があります。この点がクラスベースビューの難しいところかなぁと思います。

スポンサーリンク

slug_url_kwarg

ここまでプライマリーキーでインスタンスを1つに特定することを前提に説明をしてきましたが、DetailView ではスラッグでインスタンスを1つに特定することも可能となっています。

例えば、下記のように URL でプライマリーキーではなくスラッグの文字列を指定することで、このスラッグに対応したインスタンスのみを取得して情報を表示するようなことが可能です。

/パス/スラッグ/

ですが、これも今までの説明の通りで、このようにスラッグを指定できるようにするためには urls.py もプライマリーキーではなくスラッグを指定可能なように変更する必要があります。

例えば下記のように urls.py を変更すればスラッグを URL で指定できるようになります。そして、この際、URL で指定されたスラッグは 変数名 という変数名で扱われることになります。

urls.pyの変更(スラッグの指定)
from django.urls import path
from . import views

urlpatterns = [
    path('パス/<slug:変数名>/', views.CommentDetail.as_view()),
]

それに対し、DetailView は、URL で指定されたスラッグをクラス変数 slug_url_kwarg で定義される変数名で扱うようになっています。そして、slug_url_kwarg のデフォルト設定は 'slug' となっていますので、slug_url_kwarg を定義しない場合は、URL で指定されたスラッグを slug という変数名で扱えるよう urls.py を作成しておく必要があります。それ以外の変数名で扱うようになっている場合、DetailView が動作する際に例外が発生することになります。

そのため、例えば下記のように、urls.py でスラッグを扱う変数名を slug として定義する必要があります。

urls.pyの変更(スラッグの変数名の変更)
from django.urls import path
from . import views

urlpatterns = [
    path('パス/<slug:slug>/', views.CommentDetail.as_view()),
]

これにより、URL で指定されたスラッグを DetailView が受け取り、そのスラッグと slug フィールドが一致するインスタンスが取得され、そのインスタンスの情報が表示されるようになります。この slug フィールドとは、モデルクラスに定義するslug フィールドのことになります。

models.py
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=256)
    date = models.DateField(auto_now_add=True)
    slug = models.SlugField(max_length=256, null=True, unique=True)

要は、この Comment の場合、URL で指定したスラッグと、 slug フィールドに格納されている文字列とが一致するインスタンスが Comment のテーブルから検索され、見つかった場合にそのインスタンスが取得されることになります。この時、slug フィールドが参照されることになりますので、スラッグを URL で指定できるように urls.py を作成した場合は、インスタンス取得先のモデルクラスには slug フィールドが必要になることになります。このフィールドが存在しない場合は下記のような例外が発生することになります(後述で説明しますが、スラッグを扱うフィールドは slug から別のものに変更することも可能です)。

Cannot resolve keyword 'slug' into field. Choices are: date, id, text

ここまでが、スラッグで取得するインスタンスを1つに特定する際の説明になります。で、ここで主題として扱っている slug_url_kwarg は、前述の通り DetailVew のサブクラスがスラッグを受け取る変数名を指定するクラス変数となります。したがって、この slug_url_kwarg を変更することで、DetailView のサブクラスが受け取る変数名を変更することが可能です。

例えば、slug_url_kwarg を下記のように定義すれば、CommentDetail は comment_slug という変数名でスラッグを受け取るようになります。そして、この comment_slug という変数名の文字列が slug フィールドと一致するインスタンスを Comment のテーブルから取得し、そのインスタンスの情報が表示されるようになります。

slug_url_kwargの定義
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment
    slug_url_kwarg = 'comment_slug'

で、この場合、CommentDetailcomment_slug という変数名でスラッグを受け取るのですから、urls.py では下記のようにスラッグを comment_slug という変数名で指定できるようにしておく必要があります。

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

urlpatterns = [
    path('パス/<int:comment_slug>/', views.CommentDetail.as_view()),
]

slug_field

slug_url_kwarg が “DetailView のサブクラスがスラッグを受け取る変数名” を指定するクラス変数であるのに対し、ここで紹介する slug_field は、スラッグを格納するフィールド名を指定するクラス変数となります。

前述の通り、DetailView のサブクラスはスラッグを受け取り、そのスラッグと slug フィールドとが一致するインスタンスをテーブルから取得します。つまり、スラッグの文字列が格納されているフィールドが slug であることを前提に動作します。

スラッグを扱うフィールドがslugであることを前提にDetailViewが動作することを示す図

このように、スラッグの文字列が格納されているフィールドが slug であることを前提に動作するのは、クラス変数 slug_field のデフォルトが 'slug' となっているためになります。逆に言えば、slug_field を定義し直してやれば、他のフォールドをスラッグとして扱うことが可能となります。

例えば、slug_url_kwargslug_field を下記のように定義すれば、CommentDetail は comment_slug という変数名でスラッグを受け取るようになります。そして、この comment_slug という変数名の文字列が slug_field で指定するフィールド、下記の場合は text_slug フィールドと一致するインスタンスを Comment のテーブルから取得し、そのインスタンスの情報が表示されるようになります。

slug_fieldの定義
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment
    slug_url_kwarg = 'comment_slug'
    slug_field = 'text_slug'

この場合、Comment の text_slug フィールドがスラッグとして扱われることになるため、下記のように Comment には text_slug フィールドを定義しておく必要があります。

models.pyの変更
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=256)
    date = models.DateField(auto_now_add=True)
    text_slug = models.SlugField(max_length=256, null=True, unique=True)

この辺りも、結局は View のサブクラス と他の部分(models.pyurls.py 等)とで話が合うように開発していくことが重要となります。

context_object_name

context_object_name に関しては ListView の context_object_name と同様のクラス変数となります。要は、データベースから取得したインスタンスをコンテキストにセットするキーを指定するクラス変数となります。そして、この context_object_name に指定した文字列を変数名としてテンプレートファイルから参照可能となります。

また、コンテキストには context_object_name で指定されるキーだけでなく、'object' というキーにもインスタンスがセットされることになります。なので、テンプレートファイルからは context_object_name に指定した変数名 or object を参照して情報を出力するようにしてやれば良いことになります。

ただし、DetailView の場合はコンテキストにセットされるデータが1つのインスタンスのみとなります。ListView の場合はコンテキストにセットされるデータはインスタンスの集合(クエリセット)であり、イテラブルなオブジェクトですが、DetailView の場合は単なるインスタンスなので、そのデータに対して for 文を実行するようなことはできません。

また、context_object_name を定義しなかった場合、context_object_name にはデフォルトでインスタンスの取得先のモデルクラスの 'モデルクラス名' (小文字) が設定されることになっています。

したがって、例えば下記のようにモデルクラスを定義した場合、

context_object_nameの定義なし
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment

context_object_name が定義されていないため、テンプレートファイルからは下記のように comment を参照する or object を参照する必要があります。

commentを参照するテンプレート
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Example of DetailView</title>
</head>
<body>
    <main>
        <h2>コメント({{ comment.id }})</h2>
        <table>
            <tbody>
                <tr><td>ID   :</td><td>{{ comment.id }}</td></tr>
                <tr><td>TEXT :</td><td>{{ comment.text }}</td></tr>
                <tr><td>DATE :</td><td>{{ comment.date }}</td></tr>
            </tbody>
        </table>
    </main>
</body>
</html>

それに対し、例えば下記のように context_object_name を定義した場合、

context_object_nameの定義
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment
    context_object_name = 'comment_obj'

今度はテンプレートファイルからは下記のように comment_obj を参照する or object を参照する必要があります。comment では参照できなくなります。

comment_objを参照するテンプレート
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Example of DetailView</title>
</head>
<body>
    <main>
        <h2>コメント({{ comment_obj.id }})</h2>
        <table>
            <tbody>
                <tr><td>ID   :</td><td>{{ comment_obj.id }}</td></tr>
                <tr><td>TEXT :</td><td>{{ comment_obj.text }}</td></tr>
                <tr><td>DATE :</td><td>{{ comment_obj.date }}</td></tr>
            </tbody>
        </table>
    </main>
</body>
</html>

基本的には、context_object_name は定義せずにテンプレートファイルからは object or モデルクラス名 (小文字) を参照するようにしてやれば良いと思いますが、既にテンプレートファイルを用意しており、参照する変数名が他のものである場合は、その変数名に合わせて context_object_name を変更してやればテンプレートファイル側の変更なしにアプリを動作させることも可能となります。

スポンサーリンク

template_name

ここからは、ここまで話の中にも挙がっていたテンプレートファイルに関するクラス変数について説明していきます。まず template_name はビューから利用するテンプレートファイルのパスを指定するクラス変数となります。このパスには、アプリの templates フォルダから見た相対パスを指定します。

デフォルトでは、テンプレートファイルのパスとして アプリ名/モデルクラス名_detail.html が設定されるようになっているため、DetailView を継承するクラスが動作する際は、あらかじめこのパスにテンプレートファイルを用意しておくか、template_name の定義を行い、template_name に指定したパスにテンプレートファイルを用意しておく必要があります。

例えば、forum というアプリの views.py で下記のように CommentDetail を定義した場合、

template_nameの定義
from django.views.generic import DetailView
from .models import Comment

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

テンプレートファイルは、forum/templates/forum/comment.html に用意しておく必要があります。さらに、前述の通り、このテンプレートファイルでは context_object_name に指定した変数名 or context_object_name のデフォルト設定の変数名 or object という変数名のいずれかでインスタンスを参照するようにしておく必要があります。

template_name_suffix

先ほど、template_name を定義しなかった場合、テンプレートファイルのパスとして アプリ名/モデルクラス名_detail.html がデフォルトで設定されると言いましたが、この _detail はクラス変数 template_name_suffix を定義することで変更することも可能です。

template_name_suffix は、その名の通りデフォルトのテンプレートファイルのパスにおけるファイル名の末尾を指定するクラス変数であり、デフォルトで '_detail' が指定されています。

例えば、forum というアプリの views.py で下記のように CommentDetail を定義した場合、

template_nameの定義
from django.views.generic import DetailView
from .models import Comment

class CommentDetail(DetailView):
    model = Comment
    template_name_suffix ='info'

template_name を指定していないため、デフォルト設定のテンプレートファイルが利用されることになり、そのファイルのパスは forum/commentifno.html となりますので、このパスにテンプレートファイルを用意しておく必要があることになります。

DetailView のメソッド

次に DetailView の持つメソッドを紹介していきます。DetailView を継承したクラスを定義し、そのクラスで DetailView の持つメソッドをオーバーライドしてやることで DetalView とは異なる動作のクラスを実現することができるようになります。

まずはメソッド一覧を示し、続いて、その中からオーバーライドする機会が多そうなメソッドのみの詳細を説明するようにしたいと思います。

スポンサーリンク

DetailView のメソッド一覧

DetailView の持つメソッドの一覧は下記のようになります。あくまでもクラスのカスタマイズ目的でオーバーライドを行う可能性のあるものを挙げており、as_view などのカスタマイズは行わないであろうメソッドは省略しています。

また、DetailViewget メソッドを持っており、メソッドが GET のリクエストを受け取った場合は get が実行され、この get の中から各種メソッドが実行されるようになっています。

  • get:リクエストのメソッドが GET の場合の処理を実行する
  • get_context_data:テンプレートに渡すコンテキストを生成する
  • get_context_object_namecontext_object_name への指定値を取得する
  • get_queryset:インスタンスの集合(クエリセット)を取得する
  • get_object:インスタンスを取得する
  • get_slug_fieldslug_field を取得する
  • get_template_names:テンプレートファイルの名前を取得する
  • render_to_response:レスポンスを返却する

この中でオーバーライドする可能性が高そうなのは get_object くらいかなぁと思います。

get_object

ということで、ここでは get_object のオーバーライドについてのみ解説を行なっていきます。get_object はページに表示する1つのインスタンスを取得するメソッドになります。そして、ここで取得されたインスタンスがコンテキストにセットされ、テンプレートファイルで HTML に埋め込まれて表示されることになります。

ここまでの説明を読んでいただければ分かると思いますが、基本的に DetailView はプライマリーキー or スラッグで指定されるインスタンスを取得するようになっています。したがって、これら以外のインスタンスを取得してページに表示するようにしたい場合は、この get_object をオーバーライドする必要があります。

例えば、下記のように get_object をオーバーライドすれば、URL で指定されたスラッグが latest である場合に投稿日が一番新しい Comment のインスタンスが get_object で取得されるようになります。例としてはイマイチかもしれませんが、get_object のオーバーライドで様々なインスタンスの取得が可能となることは理解していただけるのではないかと思います。

get_objectのオーバーライド
class CommentDetail(DetailView):
    model = Comment

    def get_object(self, queryset=None):
        slug = self.kwargs.get(self.slug_url_kwarg)
        if slug != 'latest':
            return super().get_object(queryset)

        # 引数のquerysetをクラス変数のquerysetより優先する
        if queryset is None:
            queryset = self.get_queryset()

        # querysetが定義されていない場合
        if queryset is None:
            queryset = self.model.objects.all()

        # dateに対して降順に並べる
        queryset = queryset.order_by('-date')

        # 先頭の要素を取得
        obj = queryset[0]
        
        return obj

DetailView が生成するコンテキスト

続いて DetailView が生成するコンテキストについて説明しておきます。

DetailView が生成するコンテキストには下記の要素が含まれます。2つの要素が存在しますが、これらの値は両方とも同じデータとなります。

  • 'object' : データベースから取得されたインスタンス
  • context_object_name : データベースから取得されたインスタンス

context_object_name に関しては context_object_name で説明したようにクラス変数の定義で変更することも可能ですし、デフォルトは モデルクラス名 (小文字) となっています。モデルクラス名が Comment であれば 'comment' となります。

要は、DetailView が生成するコンテキストには、プライマリーキーやスラッグ等で特定される1つのインスタンスのみしかセットされていません。他のデータをテンプレートファイルから参照したい場合は、extra_context で説明しているクラス変数 extra_context を定義したり、get_context_data のオーバーライドを行なったりしてコンテキストの要素を追加する必要があります。

スポンサーリンク

DetailView の利用例

最後に、ここまでの説明のまとめとして、DetailView を継承するクラスでのビューの作成例を示していきたいと思います。

プロジェクトとアプリの作成やモデルの定義・マイグレーションの手順に関しては下記ページの ListView の利用例 と同じとなりますので、ここでは説明を省略させていただきます。ListView の解説ページの利用例を読んでいない方は、下記ページの プロジェクトとアプリの作成モデルの作成とマイグレーション を読んで手順を進めていただければと思います。

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

クラスベースビューの作成

プロジェクトやアプリ、モデルの準備が済んだ後は、このページの主題ともなっているクラスベースビューを作成していきます。今回は、ここまでの説明の通り DetailView を継承するクラスを定義し、そのクラスをクラス変数の定義とメソッドのオーバーライドによってカスタマイズしていきます。

今回は、下記のように views.py を変更することでクラスベースビューを作成したいと思います。一部関数ベースのビューがありますが、これは動作確認を楽に行うために複数の Comment のインスタンスをデータベースに新規作成するものとなっています。

views.py
from django.views.generic import DetailView
from .models import Comment
from django.http import HttpResponse

class CommentDetail_pk(DetailView):
    model = Comment
    pk_url_kwarg = 'comment_id'
    template_name_suffix = 'info'


class CommentDetail_slug(DetailView):
    model = Comment
    slug_url_kwarg = 'comment_slug'
    template_name_suffix = 'info'

def test_view(request):
    texts = [
        'Hello', 'Good bye...', 'Excellent!', 'Good night!', 'Hi!',
        'What is this?', 'Why?', 'Nice to meet you', 'Good morning',
        'See you', 'Me too', 'Yes!'
    ]

    for text in texts:
        tmp = text.lower()
        slug = ''.join(filter(str.isalnum, tmp))
        comment = Comment(text=text, slug=slug)
        comment.save()
    
    return HttpResponse(str(len(texts)) + '個のレコードを作成しました')

この views.py においては、DetailView を継承するクラスとして CommentDetail_pkCommentDetail_slug の2つを定義しています。クラス名が示す通り、前者は表示するインスタンスをプライマリーキーで指定し、後者はスラッグで指定した場合に動作させるクラスとして定義しています。なので、urls.py において、プライマリーキーが指定された際には CommentDetail_pk が、スラッグが指定された際には CommentDetail_slug が動作するように urlpattens を定義する必要があります。これに関しては後述で解説します。

2つクラスを定義しているところは複雑かもしれませんが、これらのクラスで定義しているクラス変数に関しては DetailView のクラス変数 で紹介したものばかりですので、詳細な説明は不要だと思います。

また、test_view 関数においては、Comment のインスタンスのデータベースへの新規登録を行なっています。今回、特に CommentDetail_slug では URL で指定されたスラッグが slug フィールドと一致する Comment のインスタンスを取得することになるため、Comment のインスタンスには slug フィールドが必要となります。そのため、test_view では、Comment のインスタンスに slug フィールドをセットしてから save メソッドによりデータベースへの新規登録を行うようにしています。具体的には、text フィールドの文字列から半角英数字以外を削除したものを slug フィールドにセットするようにしています。

したがって、例えば下記のように URL が指定されれば、slug フィールドに hello がセットされているインスタンスの情報が表示されることになります。

/パス/hello/

また、DetailView を継承するクラスにおいては、インスタンスが複数取得できてしまう場合は下記のような例外が発生するようになっています。

get() returned more than one Comment -- it returned 3!

プライマリーキーに関しては重複しないようになっていますが、slug フィールドに関してはフィールドの定義の仕方によっては重複する可能性があります。URL で指定されたスラッグと一致する slug フィールドを持つインスタンスが複数存在する場合は上記のような例外が発生することになるため、slug フィールドも重複しないように定義しておくことをお勧めします。

今回は、Comment を下記のように定義し、slug フィールドに unique=True を指定しているため、slug フィールドに同じスラッグがセットされた際に例外発生するようになっています。したがって、slug フィールドが重複するようなことはありません。また、slug フィールドをこのように定義しているため、test_view が複数回実行されると例外が発生することになるので注意してください。

models.py
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=256)
    date = models.DateField(auto_now_add=True)
    slug = models.SlugField(max_length=256, null=True, unique=True)

ビューと URL とのマッピング

次は、views.py で定義したビューと URL とのマッピングを行なっていきます。

まず、classviewproject フォルダの下にある urls.py を下記のように変更してください。

classviewproject/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('forum/', include('forum.urls')),
]

続いて、forum フォルダの下に urls.py を新規作成し、中身を下記のように変更してください。

forum/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('comment/<int:comment_id>/', views.CommentDetail_pk.as_view()),
    path('comment/<slug:comment_slug>/', views.CommentDetail_slug.as_view()),
    path('test/', views.test_view)
]

これらの定義より、ルートパス指定で /forum/comment/整数/ へのリクエストがあった場合に CommentDetail_pk.as_view() の get メソッドが実行され、/forum/comment/整数以外の文字列/ へのリクエストがあった場合に CommentDetail_slug.as_view()get メソッドが実行されるようになります。

スポンサーリンク

テンプレートの作成

次はテンプレートファイルを作成します。ソースコードの変更としては、これが最後となります。

まず、CommentDetail_pk でも CommentDetail_slug でもクラス変数 template_name が定義されていないため、これらのビューの両方からは DetailView でデフォルトで設定されているパスのテンプレートファイルが利用されることになります。また、template_name_suffix = 'info' が定義されているため、このデフォルトのパスは下記となります。このパスは templates フォルダから見た相対パスとなります。

forum/commentinfo.html

そのため、まず forum フォルダの下に templates フォルダを、さらに templates フォルダの下に forum フォルダを作成します。さらに、その最後に作成した forum フォルダの下に commentinfo.html を作成します。これで、CommentDetail_pkCommentDetail_slug の期待するパスにテンプレートファイルが作成されたことになります。

さらに、commentinfo.html の中身を下記のように変更して保存します。

commentinfo.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Example of DetailView</title>
</head>
<body>
    <main>
        <h2>コメント({{ comment.id }})</h2>
        <table>
            <tbody>
                <tr><td>ID   :</td><td>{{ comment.id }}</td></tr>
                <tr><td>TEXT :</td><td>{{ comment.text }}</td></tr>
                <tr><td>DATE :</td><td>{{ comment.date }}</td></tr>
                <tr><td>SLUG :</td><td>{{ comment.slug }}</td></tr>
            </tbody>
        </table>
    </main>
</body>
</html>

上記のテンプレートファイルでは次の1つの変数を参照しています。

  • commentCommentのインスタンス

comment 部分は object に変更しても OK です。ですが、commentobject 以外の他の変数名で Comment のインスタンスを参照したい場合は context_object_name をクラス変数として定義する必要があります。

また、テンプレートファイルから Comment のインスタンス以外のデータを参照したい場合は、クラス変数 extra_context の定義や get_context_data のオーバーライドを行なってコンテキストの要素を追加する必要があります。

動作確認

最後に動作確認を行なっておきましょう!

まずは、manage.py が存在するフォルダで下記コマンドを実行します。

% python manage.py runserver

これにより、開発用ウェブサーバーが起動しますので、ウェブブラウザから作成したアプリのページを表示することが可能となります。

最初に下記 URL をウェブブラウザで表示してみてください。この URL をウェブブラウザで表示すれば、12個分の Comment のインスタンスが生成され、スラッグがセットされたのちにデータベースに新規登録されるようになっています。

http://localhost:8000/forum/test/

次は、下記 URL をウェブブラウザで表示しましょう!

http://localhost:8000/forum/comment/1/

URL の最後の部分には整数を指定しているため、この URL は urlpatterns における下記の上側の行の URL パターンに一致することになります。

マッチするURLパターン
path('comment/<int:comment_id>/', views.CommentDetail_pk.as_view()), # こっちと一致
path('comment/<slug:comment_slug>/', views.CommentDetail_slug.as_view()),

そのため、上記の URL をウェブブラウザで表示する際には CommentDetail_pkget メソッドが実行されることになります。そして、上記の URL における 1 の部分はプライマリーキーとして解釈され、これと一致するプライマリーキーを持つ Comment のインスタンスがデータベースから取得されます。そして、そのインスタンスの情報が commentinfo.html に埋め込まれる形で HTML が生成され、それがページとして表示されることになります。

CommentDetail_pkによって表示されるページ1

ウェブブラウザに指定する URL の 1 部分を他の整数(12 以下)に変更すれば、また異なるページ表示が行われることが確認できると思います。これは URL で指定するプライマリーキーが変化することで、データベースから取得されるインスタンスも変化するからになります。

CommentDetail_pkによって表示されるページ2

次は、ウェブブラウザで下記 URL を開いてみましょう!

http://localhost:8000/forum/comment/goodbye/

今度は URL の最後に文字列を指定するため、

この URL は urlpatterns における下記の下側の行の URL パターンに一致することになります。

マッチするURLパターン
path('comment/<int:comment_id>/', views.CommentDetail_pk.as_view()),
path('comment/<slug:comment_slug>/', views.CommentDetail_slug.as_view()), # こっちと一致

そのため、上記の URL をウェブブラウザで表示する際には CommentDetail_slugget メソッドが実行されることになります。そして、上記の URL における goodbye の部分はスラッグとして解釈され、これと一致する slug フィールドを持つ Comment のインスタンスがデータベースから取得されます。さらに、そのインスタンスの情報が commentinfo.html に埋め込まれる形で HTML が生成され、それがページとして表示されることになります。

CommentDetail_slugによって表示されるページ

Comment ではスラッグを扱うフィールドのフィールド名を slug としていますが、異なるフィールドでスラッグを扱う場合は、それに合わせて  CommentDetail_slug にクラス変数 slug_field を定義する必要があります。

動作確認に対する説明は以上となります。

まとめ

このページでは、DetailView および DetailView を継承したクラスベースビューの作り方について解説しました!

DetailView は1つのインスタンスの詳細を表示するための View のサブクラス であり、このクラスを継承することでインスタンスの詳細を表示するページを簡単に実現できます。

DetailView を継承するクラスを利用する際の一番のポイントは、DetailView の期待する変数名でプライマリーキーやスラッグを渡せるように urls.py を作成する必要があるという点になると思います。ここさえ理解すれば DetailView を継承するクラスでのビューの実現は簡単ですので、是非この点は覚えておいてください!

このサイトでは他の View のサブクラス についても説明していますので、是非他のページも読んでみてください!

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

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