【Django】DetailViewでページネーションを実装する方法

## はじめに
Djangoでページネーション を実装する際、ListViewでは標準でページネーション機能がサポートされており、クラス変数```paginate_by```に1ページあたりに表示するデータの数を指定するだけで、テンプレートはページごとに分割されたデータを格納した変数を受け取ることができます。そして、テンプレートでは```page_obj```オブジェクトの属性やメソッドを利用して前後のページのリンク生成や表示を行うことができます。

```py
class DiaryListView(generic.Listview):
    model = Diary
    template_name = 'diary_list.html'
    paginate_by = 6 #1ページあたり6件のデータを表示

    #Diaryモデルの全件をクエリーセットに設定
    def get_queryset(self):
        diaries = Diary.objects.all()
        return diaries
```
:::note info
page_objオブジェクトの利用例
![スクリーンショット 2023-07-18 18.10.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743446/7f42c654-8819-cf11-0f75-6299aa6892ba.png)
引用)[【Django入門１２】ページネーションの基本](https://daeudaeu.com/django-pagination/#i-5)
:::

しかし、ユーザーのプロフィールページに、そのユーザーが作成した記事一覧を併せて表示させたい場合などDetailViewを継承したビューでページネーションを実装する場合はListViewとは比べて少し複雑な手順が必要になります。


## 実装する方法
結論から述べると、```MultipleObjectMixin```クラスを継承させたDetailViewを作成するか、```MultipleObjectMixin```を使わずに```get_context_data()```のオーバーライドで```Paginator()```コントラクタの実行をする方法があります。

## サンプルコード
今回は会員登録制の記事公開アプリで、ユーザーページ(DetailView)にそのユーザーが作成した記事一覧をページネーション表示するケースを想定します。

Modelはユーザー情報に関する```CustomUser```モデルと記事情報に関する```Article```モデルを用意します。
```models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

class CustomUser(AbstractUser):
    """拡張ユーザーモデル"""

    username = models.CharField(
        _("username"),
        max_length=30,
        help_text='Required 30 characters or fewer.',
        unique=True,
        error_messages={
            'unique': _("This Username already exists."),
        },)
    
    class Meta:
        verbose_name_plural = 'CustomUser'

class Article(models.Model):

    post_user = models.ForeignKey(CustomUser, verbose_name='Post User', on_delete=models.CASCADE, related_name='name',)
    title = models.CharField(verbose_name='title', max_length=50,)
    content = models.TextField(verbose_name='content', )
    created_at = models.DateField(verbose_name='created_at', auto_now_add=True,)
    updated_at = models.DateField(verbose_name='updated_at', auto_now=True)

    class Meta:
        verbose_name_plural = 'Article'

    def __str__(self):
        return self.title
```
また、テンプレートのページネーション処理部は下記のようにします。
```user.html
<!--ページネーション処理-->
    {% if is_paginated %}
    <div class="row justify-content-between align-items-center mb-4">
      <div class="col-lg">
        <nav aria-label="Bootstrap Pagination Example">
          <ul class="pagination mb-0">
            <!--前ページへのリンク-->
            {% if page_obj.has_previous %}
            <li class="page-item">
              <a class="page-link" href="?page={{ page_obj.previous_page_number }}">
              <span class="mr-1 d-none d-sm-inline-block">&larr;</span> Previous
              </a>
            </li>
            {% endif %}

            <!--ページ数表示-->
            {% for page_num in page_obj.paginator.page_range %}
              {% if page_obj.number == page_num %}
                <li class="page-item active">
                  <a class="page-link" href="#">{{ page_num }}</a>
                </li>
              {% else %}
                <li class="page-item">
                  <a class="page-link" href="?page={{ page_num }}">{{ page_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 }}">
                  Next<span class="ml-1 d-none d-sm-inline-block">&rarr;</span>
                </a>
              </li>
            {% endif %}
          </ul>
        </nav>
      </div>
    </div>
    {% endif %}
```

## MultipleObjectMixinクラスを継承する場合
```MultipleObjectMixin```クラスは、**オブジェクトのリストを表示する時に使うMixinクラス**です。特に重要なのが、DetailViewと一緒にこのクラスを継承することで、```object_list```、```is_paginated```、```paginator```、```page_obj```といったcontextを扱えるようになります。

https://docs.djangoproject.com/en/4.2/ref/class-based-views/mixins-multiple-object/

```views.py
from django.views import generic
from django.views.generic.list import MultipleObjectMixin

class UserPageView(MultipleObjectMixin ,generic.DetailView):
    template_name = 'user.html'
    model = CustomUser
    paginate_by = 6
    
    def get_context_data(self, **kwargs):
        object_list = Article.objects.filter(post_user=self.get_object())
        context = super(UserPageView, self).get_context_data(object_list=object_list, **kwargs)

        return context
```
```MultipleObjectMixin```を継承したことで、ListViewと同様に**クラス変数```paginate_by```を指定することでPaginatorクラスやPageクラスを扱えるようになります。あとはobject_listに表示させたいクエリーセットを指定してコントラクタを立ち上げるだけです。


## MultipleObjectMixinクラスを使わない場合
```views.py
from django.views import generic
from django.core.paginator import Paginator

class UserPageView(generic.DetailView):
    template_name = 'user.html'
    model = CustomUser
    
    def get_context_data(self, **kwargs):
        context = super(UserPageView, self).get_context_data(**kwargs)
        activities = self.get_related_activities()
        context['article_list'] = activities
        context['page_obj'] = activities
        return context

    def get_related_activities(self):
        queryset  = Article.objects.select_related('post_user').filter(post_user=self.object).order_by('-created_at')
        paginator = Paginator(queryset, 6) #ページ分割
        page = self.request.GET.get('page') #クエリパラメータからページ番号取得
        activities = paginator.get_page(page) #取得したページ番号のページ取得

        return activities
```

大きな特徴は```Paginator()```コントラクタを立ち上げるために、独自のクラスメソッド```get_related_activities()```を定義している点です。

ここでは

1.クエリーセットの設定
2.```Paginator()```インスタンスを作成し、ページを分割
3.クエリパラメーターから分割されたページ番号を取得
4.取得した番号のページ取得

という順番で処理を行っています。そして```get_context_data()```のオーバーライドで```get_related_activities()```を呼び出し、contextに設定することで、テンプレートでページング処理を行えるようになります。

## まとめ
・DetailViewでページネーションを実装するには```MultipleObjectMixin```を継承する方法と、継承せずに独自にページング処理を行うクラスメソッドを定義する2種類の方法がある。
・前者の方法はコード量が少なく、後者の方法は直感的に何をやっているか分かりやすい

## 参考
[Stackoverflow](https://stackoverflow.com/questions/25569551/pagination-from-a-django-detailview)