Djangoでページングをやってみた(Paginator)

### DjangoでPaginator
WEBサービスのデータ一覧画面などでコンテンツ量が多くなってくるとGoogle検索結果画面のように複数ページに分けて表示したくなります。

で、そいつをすべて手で作成しようとするとかなり混み入ったコードを書かねばならず、ずっとめんどうくさいなーと思ってたところみつけたのがPaginatorです。

### Django 公式ドキュメント
<a href='https://docs.djangoproject.com/ja/3.2/ref/paginator/#django.core.paginator.Paginator'>Paginator</a>

なおPaginatorはDjango3.1から追加された機能です。

### Paginatorの概要

基本はこれ

```py
from django.core.paginator import Paginator

page_data = Paginator(data, page_cnt)
```
data : データべースから取得したQuerysetかリストかタプル。Querysetの場合、order_byでソートしとかないと、処理一発目でワーニングメッセージが出ます。

page : 一画面あたりのデータ表示数

```
#### 重要な注
Paginatorが提供するデータをページ指定する場合は、「1」から始まります。（「0」ではありません）
```

### コード例

初っ端にわたしが試行錯誤しながらコードを書いて、何とか動いた事例をお知らせします。

view.py（関連部分）

```py
from django.core.paginator import Paginator

page = 5 # 表示したいページ

page_cnt = 10 #一画面あたり10コ表示する
onEachSide = 3 #選択ページの両側には3コ表示する
onEnds = 2 #左右両端には2コ表示する

# データを並べ替えてないと「結果が不安定だよ」というようなワーニングが表示されるのでとりあえず'id'をしてしてる
    data = database.objects.order_by('id').all() 

# paginatorのオブジェクトをつくってる
　　data_page = Paginator(data, page_cnt)
 
# paginatorのオブジェクトからページを指定した状態のオブジェクトつくってる
   data_p = data_page.get_page(page)

# 指定したページのオブジェクトからページリンク先のリストを作っている
      data_list = data_p.paginator.get_elided_page_range(page, on_each_side=onEachSide, on_ends=onEnds) 
        
   return render(requests,'templateフォルダのパス/index.html',{'data_p':data_p,'data_list':data_list})
```

django.core.paginatorはDjangoに含まれているようなので、別途installは不要でした。
ページ数や１ページ当たりの表示コンテンツ数などは変数として指定しています。

##template

index.html(部分）

```
{% include 'templateフォルダのパス/page.html' %}
```
Paginatorに関するtemplate部分は共通で使うものとして別に作っておいて、必要なtemplat（この場合はindex.html）でincludeをつかって呼び出せばより便利です。

page.html(CSSはBootstrapを利用しました）

```jinja
<nav aria-label="ページ送り">
    <ul class="pagination">
<!--　左端マークの処理（<<マークの設定と選択ページが左端（１）ならばリンクを外す）-->   
    {% if  data_p.has_previous %}
        <li class="page-item"><a class="page-link" href="?page={{data_p.previous_page_number}}>&laquo;</a></li>
    {% else %}
        <li class="page-item disabled"><a class="page-link" href='#'>&laquo;</a></li>
    {% endif %}

<!-- data_listはview.pyでpaginator.get_elided_page_rangeをつかってlistを作ったやつを渡してきてる Django3.2以降で有効-->
    {% for i in data_list %} 
<!-- 選択ページならば、activeにする -->
        {% if  data_p.number == i %}
            <li class="page-item active"><a class="page-link" href="?page={{i}}">{{i}}</a></li>
<!-- マークがELLIPSISならば、リンクを外す 　これをやらないとリンクが生きててクリックするとエラーになりますよ-->
        {% elif i == data_p.paginator.ELLIPSIS %}
            <li class="page-item disabled"><a class="page-link" href='#'>{{i}}</a></li>
<!--　リンクをはる -->
    {% else %}
            <li class="page-item"><a class="page-link" href="?page={{i}}">{{i}}</a></li>
        {% endif %}
    {% endfor %}

<!--　右端マークの処理（<<マークの設定と選択ページが右端（最終ページ数）ならばリンクを外す）-->   
    {% if  data_p.has_next %}
        <li class="page-item"><a class="page-link" href="?page={{data_p.next_page_number}}">&raquo;</a></li>
    {% else %}
        <li class="page-item disabled"><a class="page-link" href='#'>&raquo;</a></li>
    {% endif %}
    </ul>
  </nav>
```
※選択されたページ番号（この場合はi）をpegeパラメタに渡して（href="?page={{1}}）、リンク先として指定しています。


### 表示イメージ

目的はこんな感じなのが作れれば良いです。コンテンツ数がそれほど多くなければこれでもオーケー
![スクリーンショット 2021-08-31 1.30.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1739706/0b666662-b900-0251-a69c-d09aa8823bf3.png)

paginater.get_page_rangeをそのまま使って大量のコンテンツ数の場合こうなってしまいます。
![スクリーンショット 2021-08-31 6.59.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1739706/d634273b-879d-7e55-c40c-f7f02f9cbde5.png)

paginator.get_elided_page_rangeをうまく使うとこんなかんじです
![スクリーンショット 2021-08-31 1.29.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1739706/8c0108c7-e5b4-522a-fcdd-91fc707a638e.png)

### 作成されたオブジェクトでよく使うっぽいメソッドについて

```
data_p = page_data.get_page(page)  
```
pageで必要なページ数を指定すると、該当するオブジェクト返されます。こいつをtemplateに渡してforで回せば指定したページの指定した数だけ画面表示することができます。

```py
data_page = page_data.paginater.get_page_range
data_page = page_data.paginator.get_elided_page_range(pageオブジェクト、on_each_side=3,ends=2)
```
いずれページがリストっぽく使える形式になって返ってきます。

paginator.get_rangeは、ページ全部が対象となり帰ってきますので、コンテンツ量が多いとたくさんのボタンがずらりと並ぶことになり、ちょっとカッコ悪いです。

paginator.get_elded_page_rangeは指定されたページを中心にしたリストっぽいのが返ってきます。リストっぽいのが長くなれば、適当なところで"..."を自動的に入れてくれます。

on_each_sideは指定されたページの左右に表示するページ数、endsは開始と終了で表示するページ数の指定(いずれのパラメタのdefault値はそれぞれ3と2）

:::note warn
2023/12/28　追記）
最近別のサービスで使おうとして、うまく動かず試行錯誤してわかったんですが、get_page_rangeは使えなくなったみたいです。
:::


### 重要な注
get_elided_page_rangeはDjango3.2で追加されてます。

```
Paginator.ELLIPSIS
```

paginator.get_elded_page_rangeから帰ってきたリストっぽいのやつの省略された部分には"..."的な記号が設定されています。そいつからリンクを外すためにtemplateの中でforで回しているときの処理用に使える。（ピリオドが三つつながったものとは違う。コーもド探したけど見当たらなかったので、上記メソッドを使うべき）

上記template例を参照してください

### やはり詳細は公式サイトのドキュメントを読んでください。

Paginatorについて公式サイトのどこに書いてあるかがよくわからないので、忘れないようにもう一度リンクを貼っときます。

<a href='https://docs.djangoproject.com/ja/3.2/ref/paginator/#django.core.paginator.Paginator'>Paginator</a>&nbsp;忘れるな！でもほぼ英語

公式は日本語サイトですが本文は英語のままです。翻訳（中）サイトもありましたが、どちらを読んでも同じ程度によくわかりませんでしたが、提供されるさまざまなクラスやメソッド、属性を確認したいひとは、上にあるリンク先をよく読んでくださいね。

### 最後に

Paginatorをうまく使いたいといろいろと調べてみたけれど、一般に全ページをずらりと並べる式のものが多く、省略パターンについて書いてるのを見つけてもそれはゴリゴリのコードを書いてたりして処理の流れを読み解くだけで疲れてしまってたのが、英語のドキュメントを苦労しながらよんでくうちにDjango3.2から素敵な機能が提供されていることに気づきました。こいつを使うと格段にコードを書くのが楽になり、その喜びを皆様（特に初心者のひとたち）と分かち合いたく投稿した次第です。

やはりドキュメントを読むことは大切。

考え考え、試行錯誤の末にたどり着いた「とりあえず動く」コードですのでどこまで参考になるかはいささか危ういカンジですが、うまくできなくて泣きそうな人のヒントになればと思いながらの大公開です。

現場からは以上です。