【Django】ListViewの使い方とできること(2) ページネーション編

# はじめに

Djangoの標準ビューの一つでモデルを単純に利用した表示を行うListlViewが用意されています。
[前回](https://qiita.com/aqmr-kino/items/d536e08a715a9fad5720)はListViewの基本的な機能をまとめましたが、触れていなかったページネーションについてまとめてみました。

## ページネーションとは

多数のコンテンツを提供する際に、コンテンツを一定の数で区切ってページに分ける機能のことです。
（Google検索等でいう、結果が1ページ10件ずつ表示していくような感じのものです）

## 環境

- Python 3.9
- Django 3.2

## モデル

本記事では、画面表示するモデルとして下記を使用することにします。

```python:models.py
class Article(models.Model):
    title = models.CharField(max_length=128)
    content = models.TextField()
    is_published = models.BooleanField()
```

# 使い方

## ページネーションの実装

ListViewではクラス変数として、`pagenate_by`に分割数を指定するだけでページネーションを実装することができます。

この変数を設定することで、GETパラメータ引数として与えられるページ番号をオフセットとして、表示対象のモデルオブジェクトが指定数で分割されてテンプレートへ渡されます。
テンプレートからは`page_obj`変数（ページネーション管理オブジェクト）でページ状態を参照できるようになるため、これを利用してテンプレートにページ送り用のUI実装します。

モデルオブジェクトの`object_list` には、分割後に該当のページ内で有効なオブジェクト（1ページ目であればソート順に1～5番目、2ページ目であれば、6～10番目...）のみにフィルタされた状態でテンプレートへ渡されます。

アクセス時のページ番号の指定には、GETパラメータ`page`にページ番号を渡す必要があります。リクエスト時に指定されていない場合は、1ページ目を指定したものとして扱われます。
テンプレート内で次ページへのリンク等を張る際にはリンク先URLのパラメータに目的のページの指定を行います。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
    pagenate_by = 5 # モデルの分割数を追加
```

```django:article_list.html
<!DOCTYPE html>
<html>
<head></head>
<body>
    <ul>
        <!-- object_list は該当ページ内で有効なオブジェクトのみにフィルタされて渡される-->
        {% for article in object_list %} 
            <li>{{ article.title }}</li>
        {% endfor %}
    </ul>
    
    <!-- ページネーション用のUI -->
    <div class="pagination">
        <span class="step-links">
            {% if page_obj.has_previous %}
                <!-- 現在よりも前のページが存在する場合 -->
                <a href="?page=1">&laquo; 先頭</a>
                <a href="?page={{ page_obj.previous_page_number }}">前へ</a>
            {% endif %}

            <span class="current">
                <!-- 現在のページ番号と、全ページ数を表示 -->
                Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
            </span>

            {% if page_obj.has_next %}
                <!-- 現在よりも後のページが存在する場合 -->
                <a href="?page={{ page_obj.next_page_number }}">次へ</a>
                <a href="?page={{ page_obj.paginator.num_pages }}">最後 &raquo;</a>
            {% endif %}
        </span>
    </div>
</body>
</html>
```

### ページオブジェクトについて
ページネーションを有効化するとテンプレートへ自動で渡されるようになるページオブジェクト変数`page_obj`で取得できる要素としては、下記のようなものがあります。

| 要素名                   | 説明                                             |
| ------------------------ | ------------------------------------------------ |
| has_next                 | 現在より次のページが存在する場合`True`           |
| has_previous             | 現在より前のページが存在する場合`True`           |
| has_other_pages          | 現在とは他のページ（前or次）が存在する場合`True` |
| next_page_number     | 次のページ番号                                   |
| previous_page_number | 前のページ番号                                   |

## ページネーション不要な際にUIを非表示にする
テンプレートへ自動で渡される`is_paginated`変数でページネーションの有無（オブジェクト数が分割数以下（全1ページ）の場合`false`、2ページ以上になれば`true`）を取得できます。
これを利用してページネーションが不要な場合にif文を利用して表示しない対象のコードを囲む等でUIの表示切り替えができます。

```django
{% if is_paginated %}
    <!-- ページネーションのUIをここに書く -->
{% endif %}
```

## はみ出し数の指定する

使いどころの難しい機能ですが、クラス変数として`paginate_orphans`を指定するとページネーションにより分割した際に、最後となるページで中途半端にはみ出してしまう場合に最終ページだけ例外的に分割数を超えて余剰分を表示できます。

例えば、総数が17個で分割数が5の場合、通常は4ページ構成（5-5-5-2）となりますが、はみ出し数を3とすると、最終ページで最大+3個まで分割数を超えることができるようになり、3ページ構成（5-5-7）となります。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
    pagenate_by = 5 # モデルの分割数
    paginate_orphans = 3 # はみ出し数指定を追加
```

## ページ番号のGETパラメータ名を変更する

ページ数指定の受け渡し時のGETパラメータ名として`page`を利用していましたが、これを任意の名前に変更することもできます。
パラメータ名を変更する場合は、クラス変数`page_kwarg`に使用したいGETパラメータ名を指定します。

```python:views.py
from django.views.generic import ListView
from .models import Article

class ArticleListView(Listview):
    template_name = 'article_list.html'
    model = Article
    pagenate_by = 5
    page_kwarg = 'result' # パラメータ名を変更
```

## 参考
Pagination | Django documentation | Django
https://docs.djangoproject.com/ja/3.2/topics/pagination/#paginating-a-listview

Page class | Django documentation | Django
https://docs.djangoproject.com/ja/3.2/ref/paginator/#page-class