DjangoのListViewを使って、こんな感じでページをフィルタしてみた時のメモです。
ただ、以下の実装で本当に良いのか分かりませんので、何かあればご指摘ください。
環境
Modelの用意
以下の2つのModelを用意します。
- Kind
- Food
- 一覧表示用
- Kindを外部キーとして持つ
Food.name
でもフィルタを行う
myapp/models.py
from django.db import models class Kind(models.Model): name = models.CharField('Name', max_length=255) class Food(models.Model): kind = models.ForeignKey(Kind, verbose_name="品種") name = models.CharField('名前', max_length=255)
Viewの用意
ListView.modelにはモデルFood
をセットします。
また、チェックボックス用のモデルKind
は、テンプレートで使えるようにget_context_data()
を使ってcontextへとセットします。
Multiple object mixins - get_context_data() | Django documentation | Django
myapp/views.py
class FoodListView(ListView): model = Food def get_context_data(self, **kwargs): context = super(FoodListView, self).get_context_data(**kwargs) kinds = Kind.objects.all() context['kinds'] = kinds return context
テンプレートで、フィルタ用のフォームを作成
Viewから受け取ったkinds
を使ってチェックボックスを作ります。
myapp/templates/myapp/food_list.html
<form method="get" action="" name="filter_form"> <legend>絞り込み条件</legend> <div> <span>種類:</span> {% for kind in kinds %} <input type="checkbox" id="filter_kind_{{ kind.pk }}" name="kind" value="{{ kind.pk }}">{{ kind.name }} {% endfor %} </div> <div> <span>品種:</span> <input type="search" id="filter_name" name="name" placeholder="品種名"> <button id="filter">絞り込み</button> </div> </form>
Viewで、Modelのフィルタを作成
上記のフォームを使ってGETすると、Viewにクエリパラメータが渡されます。
そこで、クエリパラメータを元に、ListViewのget_queryset()
でModelのフィルタを行ったQuerySetを作成し、テンプレートへと返すように実装します。
クエリパラメータは request.GET
というQueryDictから以下の方法で取得します。
- チェックボックスは複数選択可能なため、
getlist()
で取得 - テキストボックスは単一指定なため、
get()
で取得
myapp/view.py
def get_queryset(self): # デフォルトは全件取得 results = self.model.objects.all() # GETのURLクエリパラメータを取得する # 該当のクエリパラメータが存在しない場合は、[]が返ってくる q_kinds = self.request.GET.getlist('kind') q_name = self.request.GET.get('name') # 品種での絞込は、Kind.pkとして存在してる値のみ対象とする # "a"とかを指定するとValueErrorになるため if len(q_kinds) != 0: kinds = [x for x in q_kinds if x in ["1", "2"]] results = results.filter(kind__in=kinds) # 名前での絞り込み if q_name is not None: results = results.filter(name__contains=q_name) return results
テンプレートで、フィルタしたデータを表示
Viewで作成したQuerySetは、テンプレートではobject_list
(デフォルト名)にて参照できます。
Multiple object mixins - get_context_object_name | Django documentation | Django
また、food.kind.name
のような形で外部キーのフィールド(今回は種類名)を表示できます。
python - Django foreign key relation in template - Stack Overflow
myapp/templates/myapp/food_list.html
<div id="main"> <h1>結果</h1> <table> <tr> <th>種類</th> <th>品種</th> </tr> {% for food in object_list %} <tr> <td>{{ food.kind.name }}</td> <td>{{ food.name }}</td> </tr> {% endfor %} </table> </div>
クエリパラメータとフォーム状態とを連動
ここまででフィルタはできるものの、絞り込みボタンを押すとチェックボックスやテキストボックスの値がクリアされてしまいます。
これでは使い勝手が悪いので、クエリパラメータとフォーム状態とを連動させます。
チェックボックスとの連動
クエリパラメータのkind
とHTMLタグのchecked
を連動させる必要があります。そのため、クエリパラメータの値をcheckedという文字列へと何らかの形で変換します。
テンプレートではPythonやDjangoの関数を直接呼ぶことができないことから、
- カスタムテンプレートフィルタ
- カスタムテンプレートタグ
を使い、Djangoの関数で変換できるようにします。
Custom template tags and filters | Django documentation | Django
今回はカスタムテンプレートフィルタを使ってみます。カスタムテンプレートフィルタの実装では、
- 第一引数で、自身の値
- 第二引数で、任意の値
を受け取ることができます。
そのため、テンプレートからkind.pk
とQueryDictを渡してcheckedを表示するかどうかを決めます。
myapp/templatetags/custom_filters.py
from django import template register = template.Library() @register.filter def checked(value, querydict): kinds = querydict.getlist('kind') if str(value) in kinds: return "checked" return ""
テキストボックスとの連動
こちらも同じようにして、カスタムテンプレートフィルタを使います。
myapp/templatetags/custom_filters.py
@register.filter def name(querydict): name = querydict.get('name') return "" if name is None else name
テンプレートへ、カスタムテンプレートフィルタまわりを追加
テンプレートでは
{% load custom_filters %}
を追加- チェックボックスとテキストボックスのタグに、カスタムテンプレートフィルタを追加
kind.pk|checked:request.GET
request.GET|name
を追加します。
なお、チェックボックスのカスタムテンプレートフィルタでQueryDictを渡すために、:
でカスタムテンプレートフィルタと引数を連結しています。
myapp/templates/myapp/food_list.html
{% load custom_filters %} ... <form method="get" action="" name="filter_form"> <legend>絞り込み条件</legend> <div> <span>種類:</span> {% for kind in kinds %} <input type="checkbox" id="filter_kind_{{ kind.pk }}" name="kind" value="{{ kind.pk }}" {{ kind.pk|checked:request.GET }} <!-- 追加 --> >{{ kind.name }} {% endfor %} </div> <div> <span>品種:</span> <input type="search" id="filter_name" name="name" placeholder="品種名" value={{ request.GET|name }}> <!-- 追加 --> <button id="filter">絞り込み</button> </div> </form>
ここまでの動作
全件表示
チェックボックスでフィルタ
テキストボックスでフィルタ
両方でフィルタ
フィルタを考慮したページングの追加
ここまでで、フィルタ・クエリパラメータとフォーム状態の連動ができました。
ただ、フィルタしても件数が多い場合が考えられるので、ページングも追加してみます。
今回は、過去記事のページ番号をすべて表示するタイプ
を参考に、
- Viewに
paginate_by
を追加 - テンプレートにページング部分を追加
とします。
Djangoで、Paginatorやdjango-pure-paginationを使ってページングしてみた - メモ的な思考的な
ただ、これだけではフィルタを考慮したページングとはならないため、ページ移動した場合にクエリパラメータが失われてしまいます。
その対応方法としては、
- pagination - How to paginate Django with other get variables? - Stack Overflow
- djangoのListViewで検索結果をページングするときのあれこれ - 雑記
などがありましたが、今回はstackoverflowの回答を元に、カスタムテンプレートタグを作ります。
カスタムテンプレートタグの追加
カスタムテンプレートフィルタと同様、templatetags
ディレクトリの中に入れます。
今回は、カスタムテンプレートフィルタとは別ファイル(custom_tags.py
)として作成します。
内容は、
- request.GETのようなQueryDictは更新することができないため、
copy()
を使う - クエリパラメータは
urlencode()
で取得する
とします。
myapp/templatetags/custom_tags.py`
from django import template register = template.Library() @register.simple_tag def query_string(request, page_number): querydict = request.GET.copy() querydict['page'] = page_number return querydict.urlencode()
テンプレートへ、ページングを追加
ページング部分は以前の記事とほぼ同様ですが、
- カスタムテンプレートタグを呼ぶための
{% load custom_tags %}
- ページング部分は
<a href="{% url 'my:list' %}?{% query_string request page_obj.previous_page_number %}">previous</a>
とする- urls.pyにて、
localhost:8080/mysite/
をmyappアプリのURLとしたため
- urls.pyにて、
となります。
{% if is_paginated %} <ul class="pagination"> {% if page_obj.has_previous %} <li><a href="{% url 'my:list' %}?{% query_string request page_obj.previous_page_number %}">previous</a></li> {% endif %} {% for link_page in page_obj.paginator.page_range %} {% if link_page == page_obj.number %} <li class="active">{{ link_page }}</li> {% else %} <li><a href="{% url 'my:list' %}?{% query_string request link_page %}">{{ link_page }}</a></li> {% endif %} {% endfor %} {% if page_obj.has_next %} <li><a href="{% url 'my:list' %}?{% query_string request page_obj.next_page_number %}">next</a></li> {% endif %} </ul> {% endif %}
以上で、フィルタ + ページング機能を持ったListViewができました。
ソースコード
GitHubに上げました。
thinkAmi-sandbox/Django_ListView_filter_sample
冒頭にも書きましたが、本当にこれで良いのか分かりませんので、何かあればご指摘ください。
その他
品種名を入力しない場合、クエリパラメータが&name=
となってしまいますが、今回はそのままにしておきました。
それに対応する場合は、以下のstackoverflowが参考になるかもしれません。
Don't include blank fields in GET request emitted by Django form - Stack Overflow