はじめに
この記事では、DjangoでListViewクラスを使ったページネーションを実装する方法を紹介します。
Djangoを使ってシステムを作成するときに、この機能を使うと利便性が向上しますのでお役に立てれば幸いです。
また、ここではPaginatorを使っていませんが、この記事で使用したバージョンを書いています。
違いはviews.pyのみなので、気楽に試せると思います。
環境
- Django 3.2.4 - Python 3.9.5
ファイル構成
ファイル構成です。
テンプレートファイルをひとつのディレクトリにまとめています。
ページネーション部のファイルは別ファイルにしてbase.htmlと同じ階層に置いています。
proj
┣ -- proj
┣ -- settings.py
┣ -- app
┣ -- views.py
┣ -- templates
┣ -- base.html
┣ -- pagination.html
┣ -- app
┣ -- list.html
┣ -- static
┣ -- css
┣ -- bootstrap.min.css.map
views.py
検索キーワードでモデルインスタンスの一覧を加工するためにget_querysetメソッドにページネーションを記述します。
AND検索
複数の単語をスペースで区切ってAND検索ができるようにしています。
キーワードを空欄にして検索した場合は、keyword変数が空になるため全てのデータが表示対象となります。

テンプレートで使用するpaginate_by変数
paginate_byで1ページの表示件数を指定します。
この変数はpagination.htmlでis_paginatedとして使用します。
add_message
request変数をテンプレートに渡すためにメッセージフレームワークのadd_messageを使用します。
django.core.paginatorを使用しない場合、ページネーションで使用するのはrequest変数だけなので第3変数は使いません。
サンプルではtest messageとしています。
ソース
from django.views.generic import ListView
# 絞り込み検索
from django.db.models import Q
from functools import reduce
from operator import and_
from django.contrib import messages
# 定義したモデルの読み込み
from test.models import Test
class TestlListView(ListView):
template_name = 'app/list.html'
model = Test
# 1ページに5件を表示する
paginate_by = 5
def get_queryset(self):
queryset = Test.objects.order_by('xxx', 'ooo')
keyword = self.request.GET.get('keyword')
# AND検索
if keyword:
# 全角スペースを半角スペースに置き換えた後に、キーワードを分割する。
q_list = keyword.replace(' ', ' ').split()
'''
検索するフィールドがForeignKeyの場合、
gender__name__icontains のように、ForeignKeyフィールドが参照するフィールドを指定する。
'''
query = reduce(
and_, [Q(name__icontains=q) | Q(yomigana__icontains=q) for q in q_list]
)
queryset = queryset.filter(query)
# テンプレートにメッセージを渡す。
messages.add_message(self.request, messages.INFO, 'test message')
return queryset
pagination.html
他のページでも使用するため、ページネーション部は別ファイルとします。
keyword変数
検索結果を引き継いでページネーションを行うためrequest.GET.keywordの有無で場合分けします。
request.GET.keywordのkeywordはlist.htmlで指定したinput項目のname変数を指します。
「&keyword=」のkeywordも同様です。
keyword以外の名前に変更する場合は修正してください。
各ページ番号の表示部はページ数が多くなると横長になって邪魔なのでコメントしています。
{% if is_paginated %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.keyword %}&keyword={{ request.GET.keyword }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">«</span>
<span class="sr-only">前へ</span>
</a>
</li>
<!-- 「前へ」と「次へ」の間の装飾文字 -->
<li class="page-item">
<span class="sr-only">..........</span>
</li>
{% endif %}
<!--
{% for num in paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">
{{ num }}
<span class="sr-only">(current)</span>
</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}{% if request.GET.keyword %}&keyword={{ request.GET.keyword }}{% endif %}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
-->
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.keyword %}&keyword={{ request.GET.keyword }}{% endif %}" pagearia-label="Next">
<span aria-hidden="true">»</span>
<span class="sr-only">次へ</span>
</a>
</li>
{% endif %}
<li class="p-md-2">
{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
</li>
</ul>
</nav>
{% endif %}
list.html
読み込むテンプレートファイルです。
ページネーションを記述したファイルは別ファイルなのでincludeで読み込みます。
ファイルのパスはsettings.pyのTEMPLATES変数で指定した相対パスとなります。
{% extends 'base.html' %}
{% load i18n static %}
{% block list %}
<!-- 絞り込み検索 get -->
<form action="" method="get">
{% csrf_token %}
<p class='center'>
<input class="form-control" type="text" name="keyword">
<br>
<input type="submit" class="btn btn-info" value="絞り込み検索" name="search">
</p>
</form>
<form action="" method="post">
<table class="table table-bordered table-striped">
<tr>
<th class="alert-warning">項目1</th>
</tr>
{% for f in xxx_item %}
<tr>
<td>{{ f.xxx }}</td>
</tr>
{% endfor %}
</table>
</form>
<!-- ページネーションのファイルを読み込む -->
{% include 'pagination.html' %}
{% endblock list %}
base.html
list.htmlで読み込むbase.htmlファイルです。
HTMLのヘッダやフッタ、必要なCSSファイルなどを設定します。
{% load i18n static %}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" type="image/png" href="{% static 'favicon/favicon.ico' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<title>テスト</title>
</head>
<body>
<!-- container: 表示幅を可変にする。-->
<div class="container-fluid">
{% block list %}
{% endblock %}
<!-- フッタ開始 -->
<footer class="container-fluid">
<p class="small">
フッタのテスト
<br>
Copyright© テスト
</p>
</footer>
<!-- フッタ終了 -->
</div>
<!-- end container -->
</body>
</html>
settings.py(抜粋)
テンプレートファイルをひとつのディレクトリで管理したいため、settings.pyでTEMPLATES変数を以下のように指定します。
同様に下のようにSTATICファイルもpathlibを使って指定しています。
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [ BASE_DIR / 'templates'], # Templateを一箇所にまとめる設定
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATICFILES_DIRS = [
BASE_DIR / 'static/'
]
まとめ
目的は達していますが、ページ番号の表示部分は改良の余地があると思いますので、適宜修正する予定です。


Comments