【Django】ListViewの使い方(クラスベースビューでの一覧リストページの実現)

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

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

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

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

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

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

上記ページでも解説していますが、クラスベースビューを作成していくにあたって重要になるのが、あなたが実現したいビューに合わせて継承する View のサブクラス を適切に選択し、さらにクラス変数の定義やメソッドのオーバーライドを行なってクラスをカスタマイズしていくことになります。

そして、これらを行うためには、各 View のサブクラス の特徴を理解して継承するクラスとして選択することと、その View のサブクラス で定義されているクラス変数やメソッドを理解した上でカスタマイズを行なっていくことが重要となります。

このページでは、この View のサブクラス の中から ListView に焦点を当て、このクラスの特徴や定義されているクラス変数・メソッドの紹介を行なっていきたいと思います!

ListView

ListView特定のモデルクラスのインスタンスの一覧リストを表示するページを実現する View のサブクラス となります。

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

ListViewのimport
from django.views.generic import ListView

皆さんもウェブアプリで何かしらの一覧を表示するページを作成したことがあるのではないでしょうか?

例えば掲示板アプリでの投稿コメント一覧、SNS アプリでのユーザー一覧、ゲームアプリでのランキング表などといった、特定のモデルクラスのインスタンスの一覧リストを表示するページは ListView を継承することで簡単に作成することが可能です。

ListViewの説明図

ListView は前述の通り、特定のモデルクラスのインスタンスの一覧リストを表示するページを実現するクラスとなります。そして、この表示するインスタンスはデータベースの特定のテーブルから取得してから表示されることになりますので、表示対象となるインスタンスの取得先のテーブル(モデルクラス)を指定する必要があります。もしくは、モデルクラスそのものを指定するのではなく、表示対象となるインスタンスを取得するためのクエリの指定を行うことも可能です。

いずれにせよ、こういった表示対象となるインスタンスを特定するための設定さえ行なってやれば、後は ListView が自動的にインスタンスをデータベースから取得し、それをコンテキストにセットしてテンプレートに渡すようになっています。

また、ListView は複数のインスタンスの一覧リストを表示を実現する View のサブクラス となりますが、逆に1つのインスタンスの詳細を表示する際には下記ページで紹介している DetailView を継承することになります。

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

これらの ListViewDetailView は一緒に覚えておくと良いと思います。クラスベースでビューを作成する場合、これらは必ずと言っていいほど利用する機会の多い View のサブクラス になると思います。

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

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

基本的に、クラスベースビューは views.py に View のサブクラス を継承するクラスを定義することで作成していくことになります。そして、views.py に定義したクラスにクラス変数やメソッドを定義することで、継承した View のサブクラス の特徴を活かしながら自身のウェブアプリに応じたビューにカスタマイズしていくことになります。

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

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

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

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

この辺りの、クラス変数やメソッドの定義によりカスタマイズが可能である理由については下記ページで解説していますので、詳しくは下記ページを参照してください。

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

また、前述の通り、ListView はインスタンスの一覧を表示するページを作成する際に継承するクラスです。この ListView を継承する場合、表示するインスタンスを特定するための情報として、最低限クラス変数として model or queryset を定義する必要があります(get_queryset をオーバーライドする場合は、get_queryset の処理内容によってはこれらの定義も不要となる場合があります)。

従って、ListView を継承してクラスベースビューを作成する場合、最低限下記のようにクラスを定義してやれば良いことになります。この場合、model に指定したモデルクラスの全インスタンスが取得されて表示されることになります。

ListViewの継承(modelの指定)
from django.views.generic import ListView
from .models import モデルクラス

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

もしくは、下記のように queryset の定義を行うのでも問題ないです。下記では all を実行しているので先ほど同様に モデルクラス の全インスタンスが取得されて表示されることになりますが、allfilter に置き換えれば、引数に指定した条件を満たすインスタンスのみが取得されて表示されるようになります。

ListViewの継承(modelの指定)
from django.views.generic import ListView
from .models import モデルクラス

class クラス名(ListView):
    queryset = モデルクラス.objects.all()

つまり、特定のモデルクラスの全インスタンスを取得して表示したい場合は model のみ定義してやれば良いのですが、条件等を指定してインスタンスを取得して表示したい場合は queryset を定義する必要があります。両方指定した場合は queryset 側が優先されますので、両方を指定しても問題ありません。

一応上記のように定義してやれば、このクラスはビューとしては動作することになります。ただし、上記のように model or queryset のみをクラス変数として定義している場合、ビュー以外の部分は ListView に合わせて作成する必要があります。

例えば上記のようなクラスの定義の場合、HTML 生成時に利用されるテンプレートファイルの名前は 'アプリ名/モデルクラス名_list.html’ 固定になります。また、テンプレートファイルに渡されるコンテキストには、取得されたインスタンスの集合が モデルクラス名_listobject_list というキーでコンテキストにセットされることになります(これらのキーにセットされるインスタンスの集合は同じものになります)。つまり、テンプレートファイルから参照するインスタンスの集合の変数名は モデルクラス名_list or object_list 固定となります。

MEMO

これらの名前の モデルクラス名 部分は全て小文字となります

したがって、この場合は、'アプリ名/モデルクラス名_list.html’ という名前でテンプレートファイルを用意し、そのテンプレートファイルで下記のように モデルクラス名_list or object_list から各インスタンスを参照するようにする必要があります。

アプリ名/モデルクラス名_list.html
<table>
    <thead>
        <th>ID</th><th>本文</th>
    </thead>
    <tbody>
        {% for obj in モデルクラス名_list %}
        <tr>
            <td>{{ obj.データ属性 }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>

ここで言いたいことは、ListView では利用するテンプレートファイルの名前やコンテキストにインスタンスの集合をセットするキーがデフォルト設定として決まっているため、カスタマイズを行わない限り、そのデフォルト設定に合わせてテンプレートファイル等を用意する必要があるということになります。ですが、カスタマイズを行うことで、ListView を継承するクラスが利用するテンプレートファイルの名前やコンテキストにインスタンスの集合をセットするキーを変更することも可能です。

具体的には、テンプレートファイルの名前に関しては template_name の定義によって、テンプレートファイルから参照するインスタンスの集合の変数名に関しては context_object_name の定義によって変更可能です。

したがって、これらを変更したい場合、ListView を継承するクラスは下記のように定義する必要があります(もちろん model の代わりに queryset を指定しても OK です)。

テンプレート関連のクラス変数
from django.views.generic import ListView
from .models import モデルクラス

class クラス名(ListView):
    model = モデルクラス
    template_name = テンプレートファイルの名前
    context_object_name = テンプレートから参照するオブジェクトの名前

こんな感じで、View のサブクラス を継承したクラスを定義し、さらにクラス変数の定義(もしくは後述で説明するメソッドの定義)によって、クラスの動作を自身のウェブアプリに合ったものにカスタマイズしていくのがクラスベースのビューの作り方の基本となります。

ListView を継承する場合、上記で紹介した modelquerysettemplate_namecontext_object_name のクラス変数について理解しておけば、基本的なビューであれば大体作成可能になると思います。

ただ、ビューに対して詳細なカスタマイズを行いたい場合は、もっと他のクラス変数を定義したりメソッドを定義したりする必要があります。例えば、下記ページで解説しているページネーションをクラスベースのビューで実現したいような場合は、別途クラス変数の定義を追加する必要があります。

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

このカスタマイズを行う上で重要なのは、View のサブクラス で定義されているクラス変数やメソッドを定義する必要があるという点になります。これらを定義する目的は、View のサブクラス で定義されているクラス変数やメソッドを、そのサブクラス側で上書きして View のサブクラス とは異なる動作を実現させることにあります。従って、View のサブクラス で定義されていないクラス変数やメソッドを定義してもあまり意味がありません。

では、ListView で定義されているクラス変数やメソッドにはどのようなものがあるのでしょうか?そして、それらはそれぞれどんな役割のクラス変数やメソッドなのでしょうか?

このような疑問を解消するため、ここからは ListView で定義されているクラス変数やメソッドを紹介していきたいと思います。

スポンサーリンク

ListView のクラス変数

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

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

ListView のクラス変数の一覧

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

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

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

models.py
from django.db import models

class Comment(models.Model):
    text = models.CharField(max_length=256)

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

model

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

下記ページで説明しているとおり、Django においてはモデルクラスがデータベースのテーブルを表し、インスタンスが、そのテーブルのレコードを表しています。ですので、要は model を指定することで、インスタンスの取得先のテーブルを指定することができることになります。

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

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

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

class CommentList(ListView):
    model = Comment

スポンサーリンク

queryset

単に model を指定した場合は、model に指定したテーブル(モデルクラス)の全レコード(インスタンス)が取得されて表示されることになります(後述で説明するページネーションを利用して複数のページにレコードを割り付けて表示することも可能です)。

ただ、全レコードではなく、特定の条件を満たすレコードのみを取得して表示したいような場合も多いです。そんな時にはクラス変数 queryset を定義してやれば良いです。

queryset はデータベースに発行するクエリを指定するクラス変数であり、これを定義することでデータベースに発行するクエリを変更することが可能です。そして、データベースから取得されるレコードはクエリによって決まるため、クエリの変更によって全てのレコードではなく特定のレコードのみを取得するようなことが可能となります。そして、その取得されたレコードのみがページに表示されるようになります。

さらに、このクエリは、各モデルクラスの持つ objects にメソッドを実行させることで生成することができますので、queryset には objects のメソッドの実行結果を指定することになります。

クエリの生成
queryset = モデルクラス.objects.メソッド(引数)

 メソッド として all を実行させた場合は全てのレコードが取得されることになり、filter を実行させた場合は引数で指定した条件に合致するレコードのみが取得されるようになります。

例えば、Comment のテーブルの中から、text フィールドに Hello が含まれるレコードのみを取得して表示したいような場合は、ListView を継承したクラスに対して下記のように queryset を定義してやれば良いです。

querysetの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    queryset = Comment.objects.filter(text__contains='Hello')

ordering

ordering は、その名の通りインスタンス(レコード)のソート条件を指定するクラス変数となります。ordering には、モデルクラスの持つフィールドのフィールド名を文字列で指定します。これにより、データベースから取得されたインスタンスが、そのフィールドに対して “昇順” にソートされることになります。”降順” にソートしたい場合は、フィールド名の前に - (ハイフン) を付加して指定を行います。

例えば、Comment の全インスタンスを text フィールドに対して降順に並べて表示したい場合は、ListView を継承したクラスに対して下記のように ordering を定義してやれば良いです。

orderingの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    ordering = '-text'

ordering はソート対象となるフィールドを指定するだけのものですので、ordering を単体で指定するのではなく、modelqueryset と組み合わせて指定する必要があります。そもそも ListView を継承する場合は、表示対象のインスタンスを特定するために基本的に model or queryset のどちらかを定義する必要があります。

また、ordering の場合は、クエリの変更によっても同等のことを実現することが可能です。例えば下記のように queryset を定義した場合も、先ほどと同様に Comment の全インスタンスを text フィールドに対して降順に並べて表示することが可能となります。

querysetでのソート
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    queryset = Comment.objects.order_by('-text')

context_object_name

context_object_name は、取得したインスタンスの集合をセットするコンテキストの “キー” を指定するクラス変数となります。

下記ページでも解説しているとおり、render 関数にコンテキストという辞書データを渡すことで、そのコンテキストのキーを変数名としてテンプレートファイルから参照することができるようになります。

Djangoのテンプレートの解説ページアイキャッチ 【Django入門4】テンプレートの基本

ListView を継承した場合、基本的にコンテキストは ListView が自動的に作成してくれることになります。参考に、ListView (より正確にいうと MultiObjectMixin) でコンテキストの生成を行なっている箇所のソースコードを引用して掲載しておきます。

ListViewでのコンテキストの生成
context_object_name = self.get_context_object_name(queryset)
if page_size:
    paginator, page, queryset, is_paginated = self.paginate_queryset(queryset, page_size)
    context = {
        'paginator': paginator,
        'page_obj': page,
        'is_paginated': is_paginated,
        'object_list': queryset
    }
else:
    context = {
        'paginator': None,
        'page_obj': None,
        'is_paginated': False,
        'object_list': queryset
    }
if context_object_name is not None:
    context[context_object_name] = queryset
context.update(kwargs)

上記のソースコードを確認していただければ分かるように、context_object_name に指定したキーと 'object_list' というキーの値として queryset に基づいて取得されたインスタンスの集合がコンテキストにセットされるようになっています。

MEMO

queryset をクラス変数として定義していない場合は、model.objects.all()queryset にセットされた状態で上記が実行されることになります

この 'object_list' というキーは固定になりますが、context_object_name 側に関しては context_object_name をクラス変数として定義することで変更することが可能です。

また、context_object_name を定義しなかった場合は、自動的に 'モデルクラス名_list' という名前が context_object_name として扱われるようになっています。つまり、context_object_name を定義しなかった場合、'モデルクラス名_list''object_list' というキーの値としてインスタンスの集合がセットされるようになっています。

もし、これらのキーではなく、他のキーにインスタンスの集合をセットしたい場合は、クラス変数 context_object_name を定義して他のキー名を指定してやる必要があります。

例えば、context_object_name を指定しなかった場合、Comment のインスタンスの集合を扱うテンプレートファイルでは、そのインスタンスの集合は comment_list もしくは object_list という変数名で参照する必要があります。

context_object_nameを指定しない場合
<table>
    <thead>
        <th>ID</th><th>本文</th>
    </thead>
    <tbody>
        {% for comment in comment_list %}
        <tr>
            <td>{{ comment.id }}</td>
            <td>{{ comment.text|truncatechars:20 }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>

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

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

class CommentList(ListView):
    model = Comment
    context_object_name = 'comments'

Comment のインスタンスの集合を扱うテンプレートファイルでは、そのインスタンスの集合は comments という変数名で扱うことができるようになります。

context_object_nameを指定しない場合
<table>
    <thead>
        <th>ID</th><th>本文</th>
    </thead>
    <tbody>
        {% for comment in comments %}
        <tr>
            <td>{{ comment.id }}</td>
            <td>{{ comment.text|truncatechars:20 }}</td>
        </tr>
        {% endfor %}
    </tbody>
</table>

このように、クラス変数 context_object_name の定義によって、テンプレートファイルから参照する変数名を変更することができ、好きな変数名でインスタンスの集合を参照することができるようになります。例えば事前にテンプレートファイルを作っていた場合、テンプレートが参照する変数に合わせてコンテキストのキーを指定するようなことも可能です。

重要なのは、”インスタンスの集合がセットされるコンテキストのキー” と “テンプレートファイルからインスタンスの集合を参照する変数名” が一致していることであり、これらが一致するように context_object_name を定義する or テンプレートファイルを変更する必要があります。

スポンサーリンク

extra_context

extra_context はコンテキストに任意の要素を追加するためのクラス変数となります。

各種 View のサブクラス ではコンテキストが自動的に生成されるようになっています。その際にコンテキストにセットされるキーと値は View のサブクラス によって異なります。ListView で生成されるコンテキストについては ListView が生成するコンテキスト で紹介していますので、そちらを参照していただければと思います。

そういった各種 View のサブクラス で生成されるコンテキストに要素を追加したい場合に定義するのがクラス変数 extra_context であり、このクラス変数に辞書形式のデータを指定すれば、その辞書に含まれる要素(キーと値)が View のサブクラス で生成されるコンテキストに追加されるようになります。

例えば下記のように extra_context を定義すれば、コンテキストに 'title' キーと 'message' キーが追加されることになり、これらの値をテンプレートファイルから参照することができるようになります。

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

class CommentList(ListView):
    model = Comment
    extra_context = {
        'title': 'CommentList',
        'message': 'コメント一覧を表示しています'
    }

このように、クラス変数 extra_context の定義によってコンテキストの要素を追加し、それによりテンプレートファイルから参照可能なデータを増やすことができます。ListView が生成するコンテキスト で紹介するコンテキストに足りない要素があるのであれば、クラス変数 extra_context を定義して要素を追加するようにしてください。

template_name

template_name は、ページ表示のための HTML を生成する際に使用するテンプレートファイルの名前を指定するクラス変数です。テンプレートファイルの名前というよりも、アプリ/templates からのテンプレートファイルへの相対パスと考えた方が分かりやすいかと思います。

関数ベースのビューで、HTML を生成してレスポンスとして返却する際に render 関数を利用した経験がある方もおられると思いますが、要は render 関数の第2引数に指定していたものと同じものを template_name に指定すれば良いです。また、第3引数で指定する context に関しては、 context_object_name で説明したように ListView によって生成されます。

render関数の実行
return render(request, 'forum/comments.html', context)

template_name を定義しなかった場合は、アプリ名/モデルクラス名_list.html のテンプレートファイルが HTML 生成時に利用されることになります。ですので、template_name を定義しなくても、アプリ名/モデルクラス名_list.html を用意しておけば HTML の生成とレスポンスとしての返却を行うことは可能です。

例えば下記のように template_name を指定すれば、HTML 生成時に forum/comments.html のパスに存在するテンプレートファイルが利用されることになります。さらに、context_object_name でも説明したように、そのテンプレートファイルでインスタンスの集合を参照し、各インスタンスの情報を表示するようにしてやれば、インスタンスの一覧リストを表示する HTML が生成されることになります。

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

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

ちなみに、この アプリ名/モデルクラス名_list.html_list 部分は、次に説明する template_name_suffix の指定によって変更することが可能です。

template_name_suffix

ということで、次は template_name_suffix について説明していきます。先ほど、template_name を定義しなかった場合は アプリ名/モデルクラス名_list.html というテンプレートファイルが HTML 生成時に利用されると説明しました。template_name_suffix は、その アプリ名/モデルクラス名_list.html_list 部分を指定するクラス変数となります。したがって、template_name を定義しない場合に利用されるテンプレートファイルのパスの _list 部分を他のものに変更したい場合は template_name_suffix の定義が必要となります。

例えば下記のように template_name_suffix を指定した場合、HTML 生成時には アプリ名/comments.html というテンプレートファイルが利用されるようになります。

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

class CommentList(ListView):
    model = Comment
    template_name_suffix = 's'

スポンサーリンク

template_engine

template_engine をクラス変数として定義することで、使用するテンプレートエンジンを変更することができます。

テンプレートファイルを解釈し、HTML を生成するのが基本的なテンプレートエンジンの役割であり、デフォルトでは Django に用意されているテンプレートエンジンが利用されるようになっています。

そのテンプレートエンジンをクラス変数として template_engine を定義してやることで別のものに変更することができます。

ただ、私も他のテンプレートエンジンは利用したことがないため、これ以上の詳細な説明は行いませんが、クラス変数の定義で使用するテンプレートエンジンを変更可能であることをは覚えておくと良いと思います。

response_class

response_class をクラス変数として定義することで、ビューから Django フレームワークに返却するレスポンスを変更することができます。このレスポンスとは、具体的には HttpResponse というクラスのサブクラスのインスタンスになります。例えば関数ベースのビューで return render(略) といった処理を実装したことがあると思いますが、この際も render 関数の中で HttpResponse のサブクラスのインスタンスが生成され、それがビューからの return によって Django フレームワークに返却されるようになっています。

この HttpResponse のサブクラスには様々な種類のものが存在しますが、ListView のクラス変数 response_class にはデフォルトで TemplateResponse が指定されるようになっており、これがビューから Django フレームワークに返却されるようになっています。基本はデフォルトのまま TemplateResponse が返却されるようにしておくので良いですが、TemplateResponse をカスタマイズしたいような場合は、TemplateResponse を継承するクラスを定義してカスタマイズし、そのクラスを response_class に指定してやれば良いです。

例えば下記は response_classMyTemplateResponse を指定する例となります。このように response_class を定義することで、ビューから Django フレームワークに対して MyTemplateResponse のインスタンスが返却されることになります。

この MyTemplateResponseTemplateResponse を継承するクラスであり、単に print'MyTemplateResponse' と出力した後に TemplateResponse__init__ を実行するだけの意味のないクラスになっていますが、それでも、この例で response_class の役割については理解していただけるのではないかと思います。

response_classの定義
from django.views.generic import ListView
from .models import Comment
from django.template.response import TemplateResponse

class MyTemplateResponse(TemplateResponse):

    def __init__(self, request, template, context=None, content_type=None,
                 status=None, charset=None, using=None, headers=None):

        print('MyTemplateResponse')
        super().__init__(request, template, context, content_type, status, charset, using, headers=headers)

class CommentList(ListView):
    model = Comment
    response_class = MyTemplateResponse

content_type

content_type はレスポンスのヘッダーにセットされるコンテンツタイプを指定するためのクラス変数となります。content_type を定義しなかった場合は text/html がコンテンツタイプとしてヘッダーにセットされることになります。このコンテンツタイプは、クライアントに対してレスポンスのボディが HTML であることを示す情報となっています。

このレスポンスのヘッダーにセットされるコンテンツタイプを text/html から変更したい場合は content_type をクラス変数として定義しておく必要があります。ただ、基本的には ListView はある程度 HTML を返却することを前提とした作りになっているので、content_type の定義が不要なケースが多いと思います。

例えばですが、下記のように content_type を定義した場合、

content_typeの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    content_type = 'text/plain'

レスポンスのボディとしては同じデータがクライアントに返却されることになりますが、レスポンスのヘッダーにセットされるコンテンツタイプが text/plain となるため、クライアントでは受け取ったレスポンスのボディがプレーンテキストとして扱われる可能性があります。例えば Chrome でレスポンスを受け取った場合には、下図のように HTML の中身そのものが表示されたりします。

HTMLがプレーンテキストとして表示される様子

スポンサーリンク

allow_empty

allow_empty は、データベースから取得されたインスタンスの数が 0 であることを許可するかどうかを指定するためのクラス変数になります。デフォルトでは allow_empty には True がセットされており、この場合はインスタンスの数が 0 でもページの表示が行われることになります(表示されるインスタンスの数が 0 個になるだけ)。

それに対し、allow_empty に False を指定した場合は、インスタンスの数が 0 個の場合に下図のような 404 エラーがレスポンスとして返却されるようになります。

allow_empty=Falseにより404エラーが発生する例

なので、インスタンスの数が 0 個の場合にもページを表示するようにしたいか、それともエラーとして扱うようにしたいかによって allow_empty の定義の必要性が変わることになります。後者の場合はクラス変数として allow_empty = False を指定してやれば良いです。

例えば下記は、allow_empty を定義することで queryset で指定したクエリによって取得できるインスタンスの数が 0 個の場合に 404 エラーとして扱う例となります。

allow_emptyの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    queryset = Comment.objects.filter(text__contains='test')
    allow_empty = False

paginate_by

さて、ここから紹介するクラス変数は全て、前述でも紹介した下記ページで説明しているページネーションを実現するためのクラス変数となります。

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

ページネーションを実現するためには、ここから紹介するクラス変数を必要に応じて定義する必要があります。特に、この節で扱う paginate_by に関しては定義が必須となります。逆に、ページネーションが不要である場合、ここから説明するクラス変数に関しては定義が不要となります。

まず最初に紹介するのが paginate_by で、これは各ページに割り付けるインスタンスの個数を指定するためのクラス変数となります。paginate_by を指定しなかった場合、コンテキストにセットされてテンプレートファイルに渡されるインスタンスの集合には、取得されたインスタンスが全て含まれることになります。なので、基本的には取得されたインスタンスが1ページにまとめて表示されることになります。

それに対し、paginate_by に整数を指定すれば、取得されたインスタンスが paginate_by に指定された数ごとに分割されて各ページに割り付けられるようになります。下図は paginate_by = 4 を定義したときの例となります。

取得されたインスタンスが別々のページに割り付けられる様子

例えば、各ページにインスタンスを 10 個ずつ割り付けたいのであれば paginate_by = 10 を定義してやれば良いことになります。そして、これにより、コンテキストにセットされてテンプレートファイルに渡されるインスタンスの集合には、表示しようとしているページに割り付けられたインスタンスのみが含まれることになります。

また、paginate_by を定義した場合、特定のページに対応する Page クラスのインスタンスがコンテキストの 'page_obj' というキーにセットされることになります。そのため、テンプレートファイルからはページの情報やページに割り付けられたインスタンスの集合を page_obj を参照して扱うことが可能です(通常のインスタンスの集合同様に、page_obj に対して for ループを行えば各インスタンスを1つ1つ取得しながら情報の表示等を行うことが可能となります)。

MEMO

paginate_by を定義しなかった場合同様、インスタンスの集合はコンテキストに context_object_name  or object_list のキーにもセットされることになります

ただし、これらは page_obj と違って Page クラスのインスタンスではなく、ただのインスタンスの集合となります

したがって、ページの情報に関しては page_obj を利用して取得する必要があります

さらに、コンテキストには paginator キーに Paginator のインスタンスがセットされていますので、テンプレートファイルから paginator を利用してページの総数などを取得することも可能です。特に、ページネーションによるページの分割を行い、さらに分割後の最後のページに遷移するためのリンクを設置したいような場合、こういったリンクのリンク先をテンプレートファイルから指定するために paginator の利用が必要になります。

MEMO

Paginator のインスタンスは page_obj のデータ属性からも利用可能です

少し説明が長くなりましたが、ページネーション自体やページネーション利用時のテンプレートファイルの作り方に関しては下記ページで説明していますので、詳しくは下記ページを参考にしていただければと思います。

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

例えば下記のように paginate_by を定義すれば、Comment のテーブルから全レコード(インスタンス)が取得され、取得されたインスタンスの数が 3 以上の場合、それらが 3 つずつ各ページに割り付けられることになります。

paginate_byの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    paginate_by = 3

例えば、上記の CommentList によってインスタンスの一覧を表示するページが下記 URL に存在するとすると、

http://localhost:8000/forum/comments/

上記をそのままウェブブラウザで開いた場合は、1 ページ目が表示されることになります。が、下記のようにクエリパラメーターで ?page=2 を指定してやれば 2 ページ目が表示されることになります。

http://localhost:8000/forum/comments/?page=2

同様に、?page=n を指定すれば(n は整数)、n ページ目が表示されることになります。もし n ページ目が存在しないのであれば、下の図のように 404 エラーがレスポンスされることになります。

存在しないページにアクセスした場合にレスポンスされる404エラー

ここで重要なのは、クエリパラメーターでページ番号を指定することによって、ページネーションによって分割されたページの間を遷移することができるという点になります。

page_kwarg

で、先ほどページ番号をクエリパラメーターの page によって指定可能であると説明しました。ページ番号が page によって指定可能になっているのは、ここで紹介する page_kwarg のデフォルトが 'page' に設定されているからになります。

つまり、この page_kwarg はページ番号を指定するクエリパラメーターの変数名を指定するクラス変数になります。前述の通り、page_kwarg を定義しなかった場合、ページ番号は page によって指定できるようになっています。

例えば、下記のように page_kwarg を定義した場合、ページ番号をクエリパラメーター p で指定できるようになります。

page_kwargの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    paginate_by = 3
    page_kwarg = 'p'

したがって、先ほどの例で考えれば、2 ページ目を表示する際は下記のようにクエリパラメーターを指定することになります。

http://localhost:8000/forum/comments/?p=2

スポンサーリンク

paginate_orphans

paginate_by の定義により、各ページに paginate_by に指定した個数のインスタンスが割り付けられていくことになります。この際、最後のページには中途半端に少ない個数のインスタンスのみが割り付けられる可能性があります。paginate_orphans は、そういった最後のページに割り付けられたインスタンスを、最後の1つ前のページにまとめて割り付けるようにしたい場合に定義するクラス変数になります。

より具体的には、 paginate_orphans に整数を指定して定義すれば、最後のページに割り付けられたインスタンスの数が paginate_orphans 以下の場合に、最後のページの1つ前のページにまとめて割り付けられるようになります。

例えば下記のように paginate_orphans を定義すれば、最後のページに割り付けられるインスタンスの数が 3 以下の場合は、それらのインスタンスが最後のページの1つ前のページにまとめて割り付けられるようになります。

paginate_orphansの定義
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment
    paginate_by = 5
    paginate_orphans= 3

例えば、取得されたインスタンスの数が 8 の場合、paginate_orphans を定義しなければ 1 ページ目に 5 つ、2 ページ目に 3 つのインスタンスが割り付けられることになりますが上記のように paginate_orphans を定義すれば、1 ページ目に 8 つのインスタンスが全て割り付けられることになります。

paginator_class

paginator_class はページネーションを実現するクラスを指定するクラス変数になります。paginator_class を定義しなかった場合はページネーションを実現するクラスとして Django フレームワークで定義される Paginator というクラスが利用されることになります。もし、ページネーションを Paginator 以外のクラスで実現したいのであれば、paginator_class の定義が必要となります。

これも基本的には定義は不要だとは思いますが、Paginator を継承するクラスを定義してカスタマイズしたものを利用したいような場合は、このクラス変数の定義が必要となります。

ListView のメソッド

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

ただ、特に ListView の場合はメソッドをオーバーライドする機会は少ないと思います。なので、全てに対して解説をしてもしょうがないので、まずはメソッド一覧を示し、続いて、その中からオーバーライドする機会が多そうなメソッドのみの詳細を説明するようにしたいと思います。

スポンサーリンク

ListView のメソッド一覧

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

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

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

ご覧の通り、単にクラス変数への指定値を取得するだけのメソッドが多いです。例えば get_ordering は下記のように定義されており、単にクラス変数 get_ordering の値を取得するだけのメソッドとなっています。

get_ordering
def get_ordering(self):
    """Return the field or fields to use for ordering the queryset."""
    return self.ordering

ですので、特に名前が get_クラス変数名 となっているメソッドに関しては、メソッドのオーバーライドではなくクラス変数の定義によってクラスの動作をカスタマイズすることもでき、そちらの方がカスタマイズを楽に行えると思います。

ただし、クラス変数の値はアプリ起動時に静的に決定されるため、動的にメソッドの返却値を変化させたいような場合はオーバーライドが必要になります。極端な例になりますが、例えば get_paginate_by の返却値をランダムにしたい場合は、下記のようなメソッドを、ListView を継承するクラス側に定義してオーバーライドしてやれば良いです。

get_paginate_byのオーバーライド
import random

def get_paginate_by(self, queryset):
    return random.randint(1, 10)

下記のようにクラス変数を定義した場合も上記と同じような結果が得られるようにも思えますが、この場合はアプリ起動時に random.randint が実行されて各ページに割り付けられるインスタンスの数が決まります。従って、一度アプリを起動すれば、その後はページ表示時に毎回同じ数のインスタンスが表示されることになります。

pageinate_byの定義例
class CommentList(ListView):
    model = Comment
    ordering = 'text'
    paginate_by = random.randint(1, 10)

それに対し、メソッドの場合はページ表示が行われるたびに実行されることになるため、ページを表示するたびに表示されるインスタンスの数が変化することになります。

基本はクラス変数の定義によるカスタマイズを行えば良いですが、上記の例のように、クラス変数の定義はアプリ起動時に静的に決まるため、アプリ起動後にユーザーの操作やリクエストされた URL 等に応じて動的に設定を変更したい場合はクラス変数の定義ではなくメソッドのオーバーライドで実現する必要があります。その一例として、次に get_query_set のオーバーライド例を示します。

get_queryset

では get_queryset のオーバーライドについて説明していきます。get_queryset はその名の通り、データベースに発行するクエリ(セット)を取得するためのメソッドとなります。そして、ここで取得したクエリをデータベースに発行し、クエリに応じたインスタンスの集合(単体の場合もあり)が取得されることになります。

簡単に言えば、この get_queryset メソッドは下記を返却するメソッドとなります。

  • クラス変数 queryset が定義されている場合:queryset
  • クラス変数 queryset が定義されていない場合:model.objects.all()

本当はクラス変数 ordering も考慮して返却値が決定されるようになっているのですが、説明を簡単にするため、ここではその点は考慮しないこととします。

ここで重要なのは、get_queryset はクラス変数 queryset でコントロール可能であるという点になります。queryset を定義しておけば、get_queryset の返却値はクラス変数 queryset に指定したクエリとなります。なので、クラス変数 queryset の定義で好きなクエリを指定してやれば、わざわざオーバーライドして get_queryset の動作を変化させる必要もありません。

ただし、前述の通りクラス変数は静的に決まるもの、もっと具体的に言えばアプリ起動時に決定可能なものしか指定できず、アプリ起動後に動的に変化させることができません。例えばですが、一覧リストを表示するページに検索フォームを追加し、その検索フォームにユーザーから指定された文字列を text フィールドに含む Comment のインスタンスのみを取得したい場合、クエリは下記のような処理で生成する必要があります。

検索を行うためのクエリ
Comment.objects.filter(text__contains=ユーザーから指定された文字列)

ですが、ユーザーが指定する文字列はアプリ起動時には分からないため、このようなクエリはクラス変数 queryset の定義では実現できないことになります。こういった動的に検索条件を変化させたいような場合は get_queryset をオーバーライドして get_queryset の動作を変化させる必要があります。

例えば、検索条件を動的に変化させる場合の get_queryset の定義例は下記のようなものになります。

get_querysetのオーバーライド
from django.views.generic import ListView
from .models import Comment

class CommentList(ListView):
    model = Comment

    def get_queryset(self):
        try:
            target = self.request.GET['s']
            self.queryset = self.model.objects.filter(text__contains=target)
        except Exception:
            pass
        
        return super().get_queryset()

上記では、クエリパラメーターで ?s=検索文字列 が指定された際に、その 検索文字列 が text フィールドに含まれる Comment のインスタンスのみを取得するクエリを生成して上書きするようになっています。それ以外はスーパークラス(ListView)の get_querysetComment.objects.all() に対するクエリが生成されるようになっています。ですので、検索フォームで入力された文字列がクエリパラメーターの ?s= の右辺に指定されるようにテンプレートファイルを作成しておけば、ユーザーが Comment のインスタンスの検索を行うことができるようになります。

このように、生成するクエリを動的に変更したい場合は get_queryset のオーバーライドが有効です。逆にクエリが静的に決まる場合はクラス変数 queryset の定義でクエリを指定してやれば良いことになります。

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

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

ListView が生成するコンテキストはページネーションを行うかどうかによって異なり、ページネーションを行う場合は、コンテキストに下記のような要素が含まれます。

  • 'paginator'Paginator のインスタンス
  • 'page_obj':クエリパラメーターで指定されたページに対する Page のオブジェクト
  • 'is_paginated':ページ分割されたかどうか(True or False
  • 'object_list':インスタンスの集合(クエリセット)
  • context_object_name:インスタンスの集合(クエリセット)

それに対し、ページネーションを行わない場合は、コンテキストに下記のような要素が含まれます。

  • 'paginator'None
  • 'page_obj'None
  • 'is_paginated'False
  • 'object_list':インスタンスの集合(クエリセット)
  • context_object_name:インスタンスの集合(クエリセット)

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

こういったページネーションに関するデータやインスタンスの集合はコンテキストに含まれることになりますが、他のデータをテンプレートファイルから参照したい場合は、extra_context で紹介したクラス変数 extra_context を定義してコンテキストの要素を追加する必要があります。また、extra_context の定義では静的に決まる要素しか追加できないため、追加する要素を動的に変化させたいような場合は get_context_data のオーバーライドを利用してコンテキストの要素を追加する必要があります。

MEMO

もう少し正確に言えば、各キーにセットされるデータはインスタンスそのものではなくクエリセットになるのですが、テンプレートから参照することを考えればインスタンスと捉えても問題ないかと思います

テンプレートファイルからクエリセットを利用しようとした時(クエリセットの評価が行われる時)にクエリセットのクエリがデータベースに発行され、そこでインスタンスがデータベースから取得されることになります

この辺りは下記ページで解説していますので、詳細は下記ページを参照いただければと思います

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

スポンサーリンク

ListView の利用例

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

プロジェクトとアプリの作成

このページは、View のサブクラス の紹介ページの第1弾として作成しており、各 View のサブクラス の紹介ページの中でも最初に読んでいただくことを前提としているページのため、この作成例に関しても少し詳しく説明していこうと思います。

ということで、プロジェクトの作成の仕方から説明をしていきます。説明が不要である部分はどんどんスキップしながら読み進めていただければと思います。

まず、適当な作業フォルダに移動したのち、下記コマンドでプロジェクトを作成します。今回はプロジェクトを classviewproject としています。

% django-admin startproject classviewproject

続いてアプリを作成していきます。先ほどのコマンドの実行で classviewproject というフォルダが作成されたはずなので、この classviewproject に下記の cd コマンドで移動します。

% cd classviewproject

続いて、下記コマンドを実行してアプリを作成します。今回はアプリを forum としています。

% python manage.py startapp forum

また、先ほど cd コマンドで移動後のフォルダの下にも classviewproject というフォルダがあるはずなので、そのフォルダの下にある settings.py を開き、下記のように INSTALLED_APPS のリストを変更します。これにより、アプリ forum がプロジェクトに登録されることになります。

settings.pyの変更
INSTALLED_APPS = [
    'forum', # 追加
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

この辺りのプロジェクトやアプリの関係性に関しては下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

Djangoの全体像とDjangoのウェブアプリが動作する仕組みについての解説ページアイキャッチ 【Django入門2】Djangoの全体像・ファイル構成・動作の仕組み

モデルの作成とマイグレーション

続いて forum フォルダの下にある models.py を下記のように変更します。

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 フィールドに関しては今回は利用しませんが、DetailView の解説で利用しますので、使い回しが効くよううに定義のみを行なっています。

さらに、上記 models.py に定義したモデルクラスに対応するテーブルのデータベースへの作成を行います。テーブルは下記の2つのコマンドでマイグレーションを実施することで作成することができます。

% python manage.py makemigrations
% python manage.py migrate

スポンサーリンク

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

続いて、このページの主題となっているクラスベースビューを作成していきます。今回は、ここまでの説明の通り ListView を継承するクラスを定義し、そのクラスをクラス変数の定義とメソッドのオーバーライドによってカスタマイズしていきます。

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

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

class CommentList(ListView):
    model = Comment
    ordering = 'text'
    paginate_by = 5
    paginate_orphans = 2
    template_name = 'forum/comments.html'
    page_kwarg = 'p'
    extra_context = {'title': 'コメント一覧'}

    def get_queryset(self):
        try:
            search = self.request.GET['s']
            self.queryset = self.model.objects.filter(text__contains=search)
        except Exception:
            pass
        
        return super().get_queryset()

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.replace(' ', '').lower()
        slug = ''.join(filter(str.isalnum, tmp))
        comment = Comment(text=text, slug=slug)
        comment.save()

    return HttpResponse(str(len(texts)) + '個のレコードを作成しました')

CommentList の定義を見ていただければ分かるように、定義しているクラス変数は全て ListView のクラス変数 で紹介したものになりますし、定義しているメソッドも全て ListView のメソッド で紹介したものになります。ですので、詳細な説明はここでは省略させていただきます。

ビューと 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('comments/', views.CommentList.as_view(), name='comments'),
    path('test/', views.test_view)
]

これらの定義より、ルートパス指定で /forum/comments/ へのリクエストがあった場合に CommentListget メソッドが実行されるようになります。

関数ベースビューの場合、上記の views.test_view のように path の第2引数には直接 views.py に定義した関数を指定すれば良いのですが、クラスベースビューの場合は上記の views.CommentList.as_view() のように views.py に定義したクラスの as_view() メソッドの実行結果を指定する必要があります。ここもクラスベースビューを扱う上でのポイントの1つとなります。

テンプレートの作成

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

クラスベースビューを扱う上ではテンプレートファイルの作り方も重要となってきます。テンプレートファイルとビューは密接に関わっており、双方で話が合うように両方を作成する必要があります。例えば、ビューが用意したコンテキストに存在しないキーの変数をテンプレートファイルが参照すると上手くデータの表示などができません。また、ビューが利用しようとしているテンプレートファイルが用意されていない場合もページの表示が出来なくなってしまいます。

ビューが期待するテンプレートを用意する or テンプレートが期待するビューを作成するように心がけることが重要です。

ということで、先ほど定義した CommentList と話が合うようなテンプレートファイルを作成していきたいと思います。

まず、CommentList では template_name = 'forum/comments.html' が定義されているため、templates フォルダから見た相対パスが forum/comments.html となる位置にテンプレートファイルを用意する必要があります。

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

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

comments.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>ListViewの利用例</title>
</head>
<body>
    <main>
        <h2>{{ title }}(全{{ page_obj.paginator.count }}件)</h2>
        <table>
            <thead>
                <tr>
                    <th>ID</th><th>本文</th>
                </tr>
            </thead>
            <tbody>
                {% for comment in page_obj %}
                <tr>
                    <td>{{ comment.id }}</td>
                    <td>{{ comment.text }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        <div>
            <span>
                {% if page_obj.has_previous %}
                    <a href="?p=1">« first</a>
                    <a href="?p={{ page_obj.previous_page_number }}">previous</a>
                {% endif %}
                <span>
                    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>
        <div>
            <form action="{% url 'comments' %}" method="get">
                <p><input type="text" name="s"><input type="submit" value="検索"></p>
            </form>
        </div>
    </main>
</body>
</html>

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

  • title:ページの見出し用
  • page_obj:表示中のページに対応する Pageのインスタンス

つまり、これらの変数がテンプレートファイルから参照できるように、CommentList はコンテキストを準備する必要があります。ListView が生成するコンテキスト で説明したように、page_obj に関しては、クラス変数として paginate_by を定義しておけば自動的にコンテキストにセットされるようになっています(逆に paginate_by を定義しなければ page_obj はテンプレートファイルから利用できません)。

それに対し、title に関しては自動的にコンテキストにセットされるようになっていないため、CommentList ではクラス変数 extra_context の定義によって title をコンテキストにセットするようにしています。

また、上記テンプレートファイルの最後の <div></div> では検索フォーム要素の表示を実現しています。この検索フォームでは、検索 ボタンをクリックすれば、下記のようにフォームに入力した文字列がクエリパラメーターの変数 s にセットされた状態で /forum/comments/ にリクエストが送信されるようになっています。

/forum/comments/?s=入力された文字列

そして、このリクエストを受けった際には、urls.py の設定によって再度 CommentListgetメソッドが実行されるようになっています。

さらに、CommentListgetメソッドからは CommentList で定義した get_queryset が実行されます。受け取ったリクエストではクエリパラメーターとして s が指定されているため、get_queryset では “この s に指定された文字列を text フィールドに含むインスタンスのみ” を取得するためのクエリが生成されることになります。そのため、検索フォームで 検索 ボタンがクリックされた際には、”フォームに入力された文字列を text フィールドに含むインスタンスのみ” が表示されることになります。つまり、検索機能が実現されていることになります。

この検索機能の実現に関しても、どんなクエリパラメーターで検索文字列を扱うのかをテンプレートファイルとビューとで上手く話を合わせるという点が重要となります。

ここまで説明してきたように、テンプレートファイルとビューとで上手く話が合うように双方を作成していくことが重要になります。特にクラスベースビューの場合は、継承する View のサブクラス (今回の場合は ListView) によってコンテキストなどが自動的に作成されるようになっているため、テンプレートファイルから参照する変数の変数名に関しては注意が必要となると思います。

スポンサーリンク

動作確認

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

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

% python manage.py runserver

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

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

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

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

http://localhost:8000/forum/comments/

この URL を表示することによって、views.py に定義した CommentList が動作し、Comment のインスタンス一覧が表示されることになります。

ただし、ここで表示されるのは 5 件分のインスタンスのみとなります。これは、paginate_by = 5 の定義によって各ページに 5 件分のインスタンスが割り付けられるようになっているためです。

1ページ目に表示されるインスタンスの一覧

また、各インスタンスは text に対してアルファベット順で昇順にソートされた状態で表示されるようになっています。これは ordering = 'text' を定義しているためになります。

ここで、next リンクをクリックすれば、次のページが表示され、先ほどとは異なるインスタンスが表示されることになります。

2ページ目に表示されるインスタンスの一覧

nextリンクをクリックした後に URL を見てみると ?p=2 というクエリパラメーターが確認できると思います。これは、page_kwarg = 'p' の定義により、クエリパラメーターでページ番号を扱う変数が p であることを指定しているからになります。

また、next リンククリック後に表示されるインスタンスの数が 5 件ではなく 7 件になっているのは、paginate_orphans = 2 の定義により、最後のページに割り付けられるページが 2 件以下の場合は前のページにまとめて割り付けるよう指定されているからになります。

最後に、ページ下部にある検索フォームに Good を入力して 検索 ボタンをクリックしてみてください。これにより、本文に Good が含まれるインスタンスのみが表示されるようになったことが確認できると思います。

検索して取得されたインスタンスのみが表示される様子

これは、CommentList で定義した get_queryset により、データベースから text フィールドに Good が含まれる Comment のインスタンスのみを取得するようクエリが生成されたからになります。

これらの動作確認結果より、CommentList で定義したクラス変数やメソッドに応じたページの表示が行われていることを確認できたと思います。こんな感じで、ListView で定義されているクラス変数やメソッドを、ListView を継承するクラス(今回の場合は CommentList)で上書きしてやれば、ListView の特徴を引き継いだまま、ListView とは異なる動作となるクラスを実現することができます。そして、これらを行うことで、自身が開発したいウェブアプリに応じたビューにカスタマイズしていくことが可能となります。

今回紹介した CommentList も、クラス変数とメソッドの定義の削除や変更を行えば、また異なったページの表示結果になることを確認できると思います。是非色んなカスタマイズを試してみてください!

まとめ

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

ListView は特定のモデルクラスのインスタンス一覧を表示するための View のサブクラス であり、このクラスを継承することでインスタンス一覧を表示するページを簡単に実現できます。

また、ListView では様々なクラス変数・メソッドが定義されており、ListView を継承したクラスでこれらを上書き(オーバーライド)することでページの表示内容をカスタマイズすることも可能です。このカスタマイズの仕方は他の View のサブクラス でも同様ですので、是非この考え方は覚えておいてください!

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

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

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