Django:ページネーションのページ番号を良い感じで縮める方法
Django:ページネーションのページ番号を良い感じで縮める方法
先回の記事でページネーションの大枠については記載しました。
今回はこのページネーションを良い感じでコントロールする方法についてです。
ページネーションは1回作ってしまえば includeで呼び出して使いまわせる
ちゃんと作ってしまった方が(精神衛生上)楽な理由がコレです。
サイト内でページネーションのデザインを大きく変えることはないと思います。
DjangoではHTMLのIncludeが簡単にできるので「ページネーション作っちゃえばいいじゃん」となるわけです。
この時の注意事項が2点あります。
- Templateに渡すListのデータ名(オブジェクト名)を共通にしておく
- 検索用の引数はGETで渡す
共通の名前でないとオブジェクト名の変更が必要になるからです。
プリセットClassのListViewを使う場合【object_list】という名称でTemplateに送られるので、この名称をそのまま使うのが最も汎用性が高くなります。
GETかPOSTかという点については、検索用引数をPOSTで渡してもOKです。
が、ページ遷移の際に毎回POSTする必要があるのでGETの方が簡単です。
セキュリティー的にNGの場合を除いてデータ検索の項目はGETで渡した方が良いと思います。
前回記事でも書きましたが、GETで渡した値をTemplateに戻す方法は下の通りです。
■Veiws
from urllib.parse import urlencode
def HogeListView(request,num=1):
(中略)
#getの値はrequest.GETで取り込まれる
#これをURLエンコードするとGETの姿そのままTemplateに渡せる
get_request = urlencode(request.GET)
params = {
'object_list': object_list.get_page(num),
'get_request': get_request
}
(後略)
何も考えずにpage_rangeをforループして表示すると件数によっておかしな状態に...

さすがにコレはないですよね。
と言う事でルール作りから書いてみたいと思います。
省略されたページネーションを作成するルール作り
作業の段取りはこんな感じになると思います。
- 取得できる値を整理する
- 取得できる値を使って自分のサイトにあった画面構成を考える
- 標準と例外を考えルールの概要を整える
- 簡単に実装する(場合分けパーツのみで構成してみる)
- 問題なければしっかりとした実装を行う
- 細かな修正を行う
- 完了
Templateで取得できる値を整理する
- リスト(テーブルの値:DB) => object_listで渡した時
- ページネーション前頁番号 => object_list.previous_page_number
- ページネーション次頁番号 => object_list.next_page_number
- ページネーション頁リスト => object_list.paginator.page_range
- ページネーション前頁あるか => object_list.has_previous
- ページネーション次頁あるか => object_list.has_next
- 現在のページネーション番号 => object_list.numbers
- 最終のページネーション番号 => object_list.paginator.num_pages
取得できる値を使って自分のサイトにあった画面構成を考える
こんな構造で考えたいと思います。

一番左側をクリックするとpage=1、一番右側をクリックするとpage=lastに飛ぶように仕掛けます。
で、真ん中は38個並べたりせず前後を省略。現在のページ番号を挟んで毎回前後2頁を選べる姿にします。
標準と例外を考えルールの概要を整える
現在のページ番号は【object_list.numbers】で取り出せます。
これを中心にして前後2ページを並べようとした場合、5つの並びは下のようになります。
■Normalルート
- 左から1つ目|object_list.numbers - 2
- 左から2つ目|object_list.numbers - 1
- 真ん中 |object_list.numbers
- 左から4つ目|object_list.numbers + 1
- 左から5つ目|object_list.numbers + 2
次に例外を考えてみます。object_list.numbers = 2の時はどう表示しましょう。
方向性としては2つ考えられます。

▼Normalルートのルールを変更する ※ボタン総数を維持する

6ページ目まで存在するのに4で終わるというのはチョット不満です。なので、5という枠を維持する方向で考えます。
■Underルート
object_list.numbers = 2の時のボタンの並びは下のようになります。
- 左から1つ目|object_list.numbers - 1
- 左から2つ目|object_list.numbers
- 真ん中 |object_list.numbers + 1
- 左から4つ目|object_list.numbers + 2
- 左から5つ目|object_list.numbers + 3
■Highルート
これは最終頁の1つ前のページネーションを表示しているときも同じです。
- 左から1つ目|object_list.number - 3
- 左から2つ目|object_list.number -2
- 真ん中 |object_list.number -1
- 左から4つ目|object_list.number
- 左から5つ目|object_list.number +1
Under/Highともに現在値からの差異 = 2で中央に来るのでココに分岐を付ければよさそうです。
■総ページ数が少ないルート
検索結果が5ページ以下の時も当然あります。
この時、5ページ分もページネーションを表示していても中身のないボタンが配置されることになります。
なので、5ページ以下の時には存在するページ全てを並べた方がよさそうです。
▼全4ページの時の表示

この構造なら38ページ分のリンクが出来た構成【object_list.paginator.page_range をforループ】で実装できます。
■1ページしかないルート
検索結果が1件の場合、ページネーションを書くかを検討します。
個人的には「無くていいかな」と思うので、1ページしかないときはページネーションを表示しないことにします。
簡単に実装する(場合分けパーツのみで構成してみる)
今までの考察で、分岐のポイントが明確になりました。
- ページ総数が1より大きいか
- ページ総数が6以上か否か
- ページ総数が6以上の時、現在のページ番号は2以下か
- ページ総数が6以上の時、現在のページ番号は [ 最終頁-2 ]以上か
この構成で分岐の骨組みだけ作ります。
{% load mathfilters %}
{% if object_list.paginator.num_pages > 1 %}
<!-- 2ページ以上ある場合 -->
2ページ以上あり
{% if object_list.paginator.num_pages > 5 %}
<!-- 総ページ数が6 以上 -->
※ボタン5つの構成
{% if object_list.paginator.num_pages|sub:object_list.number < 2 %}
<!-- 最終ページから-2ページ以内 -->
※ [Lastページ-5]~Lastページを並べる
※ 最終ページ-最終ページ=0なので、カウント0から始まるため2ページ以内は「2より小さい」と表現
{% elif object_list.number < 3 %}
<!-- 現在のページが2ページ以内 -->
※ 1~5を並べる
※ ページカウントは1から始まるので2ページ以内は「3より小さい」と表現
{% else %}
<!-- 現在のページが3以上かつLast_3以下 -->
※標準のボタン×5構成
{% endif %}
{% else %}
<!-- 総ページ数が5以下 -->
※2~4個のボタン構成
{% endif %}
{% else %}
<!-- 1ページしかない -->
※この時はページネーションを表示しない
{% endif %}
チョット解説
1行目に【 {% load mathfilters %} 】というコードが入っています。
これは Django-mathfilters というプラグインを有効化するためのコードです。
Django-mathfilters はTemplate.html内で計算をできるようにするプラグインです。
そのため、ページ計算の際にJavaScriptに値を投げようと考える場合、このコードは不要です。
上記例でDjango-mathfiltersを使っている個所は10行目になります。
値を入れてしっかり実装してみる
想定通り動いたら実際のデータを付け足して形を作っていきます。
この段階で『絵に描いた餅ではなかったか』『食べれる餅にするために何が必要か』が明確になります。
例えば、上の段階で存在しなかった【先頭に移動】【末尾に移動】を付け加えてみます。
■先頭に移動する
<!-- 先頭に移動 -->
<a href="{% url 'Hoge:hoge_list' num=1 %}?{{get_request}}">
<span>«</span>
</a>
■末尾に移動する
<!-- 末尾に移動 -->
<a href="{% url 'Hoge:hoge_list' num=object_list.paginator.num_pages %}?{{get_request}}">
<span>«</span>
</a>
通常だとこんな府に記載します。
しかし、ページネーションを共有(includeで複数ページで使いまわす)したいと考えているのに {% url ' Hoge:hoge_list' %}と遷移先を指定していたら共有できません。
そこでアイディアを出します。
「Viewで共通の形にしてTemplateで読み込ませるとどうだろうか...」
#view
""" URLのリンク名を「l_page」にして送る """
params = {
....
'l_page': 'Hoge:hoge_list'
}
<!-- Template(paginatore) -->
<a href="{% url l_page num=1 %}?{{get_request}}">
<span>«</span>
</a>
上手く動きました。このアイディアで問題なさそうです。
CSSなどもこの段階でつけていきます。
自分で作るのも面倒なので、沢山の良さげなデザインサイトさんから流用させてもらって取り付けると、Codeはこんな姿になりました。
{% load mathfilters %}
{% if object_list.paginator.num_pages > 1 %}
<!-- 2ページ以上ある場合 -->
<div class="pager">
<ul class="pagination">
<li class="pre">
<a href="{% url l_page num=1 %}?{{get_request}}">
<span>«</span>
</a>
</li>
{% if object_list.paginator.num_pages > 5 %}
<!-- 総ページ数が6以上 -->
{% if object_list.paginator.num_pages|sub:object_list.number < 2 %}
<!-- 現在が最終ページから2ページ以内 -->
{% with ''|center:5 as range %}
{% for list in range %}
{% if object_list.paginator.num_pages|sub:4|add:forloop.counter0 == object_list.number %}
<li>
<a class="active" href="{% url l_page num=object_list.paginator.num_pages|sub:4|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.paginator.num_pages|sub:4|add:forloop.counter0}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=object_list.paginator.num_pages|sub:4|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.paginator.num_pages|sub:4|add:forloop.counter0}}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% elif object_list.number < 3 %}
<!-- 現在が先頭から2ページ以内 -->
{% with ''|center:5 as range %}
{% for list in range %}
{% if forloop.counter == object_list.number %}
<li>
<a class="active" href="{% url l_page num=forloop.counter %}?{{get_request}}">
<span>{{forloop.counter}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=forloop.counter %}?{{get_request}}">
<span>{{forloop.counter}}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% else %}
<!-- 現在のページが3以上かつLast_3以下 -->
{% with ''|center:5 as range %}
{% for list in range %}
{% if object_list.number|sub:2|add:forloop.counter0 == object_list.number %}
<li>
<a class="active" href="{% url l_page num=object_list.number|sub:2|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.number|sub:2|add:forloop.counter0}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=object_list.number|sub:2|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.number|sub:2|add:forloop.counter0}}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
{% else %}
<!-- 総ページ数が5以下 -->
{% for page_num in object_list.paginator.page_range %}
{% if object_list.number == page_num %}
<li>
<a href="{% url l_page num=page_num %}?{{get_request}}" class="active">
<span>{{page_num}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=page_num %}?{{get_request}}">
<span>{{page_num}}</span>
</a>
</li>
{% endif %}
{% endfor%}
{% endif %}
<li class="last">
<a href="{% url l_page object_list.paginator.num_pages %}?{{get_request}}">
<span>»</span>
</a>
</li>
</ul>
</div>
{% endif %}
いい感じに実装できました。
チョット解説 [Templateでの for i~ ループ]
{% with ''|center:5 as range %}
{% for list in range %}
{{ forloop.counter0 }}
{{ forloop.counter }}
{% endfor %}
{% endwith %}
今回こんな形で書きました。
これは、with で繰返しの回数分の空リスト([ '','','','',''])を作成。forで回せば欲しい回数だけループしてくれるという手法です。
繰り返した回数は {{ forloop.counter0 }}=ゼロからカウント、{{ forloop.counter }}=1からカウントで取得できます。
{% for list in object_list.paginator.page_range|slice:"(object_list.number|sub:2):(object_list.number|add:2)" %}
#"(object_list.number|sub:2):(object_list.number|add:2)" の箇所は取り出すレンジで slice:"5:9" という姿を作ろうとしている
<li>
<a href="{% url l_page num=list %}?{{get_request}}">
<span>{{list}}</span>
</a>
</li>
{% endfor %}
Withなんて使わなくてもrenge指定のforループで取り出せると思ったのですが、sliceの指定に計算式を入れる必要があり意図した姿に実装する事が出来ませんでした。
細かな修正
使ってみるとデザイン的に「ちょっとな」と思う箇所が出てきます。
上に記したScriptだとこんなところです。
- 1ページ目やラストページの時に【先頭へ移動】【Lastへ移動】を class="active"にした方が良い
- 最終ページが何ページかわからないので、Lastへ移動は » で表すよりページ数が良いかも
- デザイン的に先頭へ移動とLastへ移動のルールは共通が良い(片方だけ≪表記とかはカッコ悪い)
修正稿がこちら(paginator.html)
{% load mathfilters %}
{% if object_list.paginator.num_pages > 1 %}
<div class="pager">
<ul class="pagination">
<li class="pre">
{% if object_list.number == 1 %}
<a class="active" href="{% url l_page num=1 %}?{{get_request}}">
<span>«</span>
</a>
{% elif object_list.number < 4 %}
<a href="{% url l_page num=1 %}?{{get_request}}">
<span>«</span>
</a>
{% else %}
<a href="{% url l_page num=1 %}?{{get_request}}">
<span>1</span>
</a>
{% endif %}
</li>
{% if object_list.paginator.num_pages > 5 %}
{% if object_list.paginator.num_pages|sub:object_list.number < 2 %}
{% with ''|center:5 as range %}
{% for list in range %}
{% if object_list.paginator.num_pages|sub:4|add:forloop.counter0 == object_list.number %}
<li>
<a class="active" href="{% url l_page num=object_list.paginator.num_pages|sub:4|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.paginator.num_pages|sub:4|add:forloop.counter0}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=object_list.paginator.num_pages|sub:4|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.paginator.num_pages|sub:4|add:forloop.counter0}}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% elif object_list.number < 3 %}
{% with ''|center:5 as range %}
{% for list in range %}
{% if forloop.counter == object_list.number %}
<li>
<a class="active" href="{% url l_page num=forloop.counter %}?{{get_request}}">
<span>{{forloop.counter}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=forloop.counter %}?{{get_request}}">
<span>{{forloop.counter}}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% else %}
{% with ''|center:5 as range %}
{% for list in range %}
{% if object_list.number|sub:2|add:forloop.counter0 == object_list.number %}
<li>
<a class="active" href="{% url l_page num=object_list.number|sub:2|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.number|sub:2|add:forloop.counter0}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=object_list.number|sub:2|add:forloop.counter0 %}?{{get_request}}">
<span>{{object_list.number|sub:2|add:forloop.counter0}}</span>
</a>
</li>
{% endif %}
{% endfor %}
{% endwith %}
{% endif %}
{% else %}
{% for page_num in object_list.paginator.page_range %}
{% if object_list.number == page_num %}
<li>
<a href="{% url l_page num=page_num %}?{{get_request}}" class="active">
<span>{{page_num}}</span>
</a>
</li>
{% else %}
<li>
<a href="{% url l_page num=page_num %}?{{get_request}}">
<span>{{page_num}}</span>
</a>
</li>
{% endif %}
{% endfor%}
{% endif %}
<li class="last">
{% if object_list.number == object_list.paginator.num_pages %}
<a class="active" href="{% url l_page object_list.paginator.num_pages %}?{{get_request}}">
<span>»</span>
</a>
{% elif object_list.paginator.num_pages|sub:object_list.number < 3 %}
<a href="{% url l_page object_list.paginator.num_pages %}?{{get_request}}">
<span>»</span>
</a>
{% else %}
<a href="{% url l_page object_list.paginator.num_pages %}?{{get_request}}">
<span>{{object_list.paginator.num_pages}}</span>
</a>
{% endif %}
</li>
</ul>
</div>
{% endif %}
まとめ
ページネーションにはいろんなやり方があります。
お客さんによって要望が異なることもあると思います。
そんな時、自分で作れればどうとでも対処できるので django.core.paginator での構築をまとめてみました。
しかし、一個作れば後は include するだけって楽ですよね。

