【Django】ページネーション機能を実装する|簡単な掲示板アプリの作成

Djangoの標準機能「Paginator」を使い、掲示板にページネーション(ページ送り)機能を実装する方法を解説します。ビューで大量の投稿データをページごとに分割し、テンプレートで「次へ」やページ番号といったリンクを動的に表示する手順を学びます。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Python: 3.10.11
  • Django: 5.0.3

サンプルコード

/app/views.py

/app/views.py
1 from django.core.mail import send_mail, EmailMessage
2 from django.conf import settings
3 from django.template.loader import render_to_string
4+from django.core.paginator import Paginator
5 
6 def user_owns_board(view_func):
7     @wraps(view_func)
8     user = request.user
9 
10     if user.is_authenticated:
11-        boards = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at')
12+        boards_query = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at')
13     else:
14-        boards = Board.objects.all().order_by('-updated_at')
15+        boards_query = Board.objects.all().order_by('-updated_at')
16+
17+    paginator = Paginator(boards_query, 10)
18+    page_number = request.GET.get('page')
19+    boards = paginator.get_page(page_number)
20+
21     return render(request, 'index.html', {'boards': boards})
22 
23 @login_required
24

/templates/index.html

/templates/index.html
1         <a href="{% url 'new' %}" class="btn btn-primary">新規投稿作成</a>
2         <a href="{% url 'my_boards' %}" class="btn btn-primary">自分の投稿一覧</a>
3     </section>
4+
5+    <!-- ページネーションのリンクを表示 -->
6+    <section class="pagination justify-content-center">
7+        <ul class="pagination">
8+            {% if boards.has_previous %}
9+                <li class="page-item">
10+                    <a class="page-link" href="?page=1">&laquo; 最初</a>
11+                </li>
12+                <li class="page-item">
13+                    <a class="page-link" href="?page={{ boards.previous_page_number }}">前へ</a>
14+                </li>
15+            {% endif %}
16+
17+            <li class="page-item disabled">
18+                <a class="page-link" href="#">ページ {{ boards.number }} / {{ boards.paginator.num_pages }}</a>
19+            </li>
20+
21+            {% if boards.has_next %}
22+                <li class="page-item">
23+                    <a class="page-link" href="?page={{ boards.next_page_number }}">次へ</a>
24+                </li>
25+                <li class="page-item">
26+                    <a class="page-link" href="?page={{ boards.paginator.num_pages }}">最後 &raquo;</a>
27+                </li>
28+            {% endif %}
29+        </ul>
30+    </section>
31 {% endblock %}
32

コード解説

変更点: ビューでPaginatorを使い、投稿一覧をページ分割する

/app/views.py
1+from django.core.paginator import Paginator
2 
3 def user_owns_board(view_func):
4     @wraps(view_func)
5     user = request.user
6 
7     if user.is_authenticated:
8-        boards = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at')
9+        boards_query = Board.objects.annotate(is_favorite=Count('favorite', filter=models.Q(favorite__user=user))).order_by('-updated_at')
10     else:
11-        boards = Board.objects.all().order_by('-updated_at')
12+        boards_query = Board.objects.all().order_by('-updated_at')
13+
14+    paginator = Paginator(boards_query, 10)
15+    page_number = request.GET.get('page')
16+    boards = paginator.get_page(page_number)
17+
18     return render(request, 'index.html', {'boards': boards})

Djangoに標準で備わっているページネーション機能を使うため、まずdjango.core.paginatorからPaginatorクラスをインポートします。

次に、データベースから取得した全投稿データを、これまでのboardsからboards_queryという変数名に変更しています。これは、ページ分割する「前」の全データであることを分かりやすくするためです。

paginator = Paginator(boards_query, 10)の部分で、Paginatorクラスのインスタンスを生成しています。第1引数にはページ分割したいデータのリスト(今回は全投稿データであるboards_query)、第2引数には1ページあたりに表示したい件数(今回は10件)を指定します。

page_number = request.GET.get('page')では、ユーザーがアクセスしたURLからページ番号を取得しています。例えば、http://example.com/?page=2というURLであれば、page_numberには2が代入されます。.get()を使うことで、URLにpageパラメータが存在しない場合でもエラーにならず、Noneが返ります。

最後に、boards = paginator.get_page(page_number)で、指定されたページ番号に該当する投稿データだけをboards変数に格納しています。このget_pageメソッドは非常に便利で、指定されたページ番号が存在しない場合は自動的に最初のページを、ページ番号が数字でないなどの無効な値の場合は最後のページを返してくれます。このboardsが、最終的にテンプレートへ渡されるデータとなります。

変更点: テンプレートにページネーションのリンクを追加する

/templates/index.html
1+    <!-- ページネーションのリンクを表示 -->
2+    <section class="pagination justify-content-center">
3+        <ul class="pagination">
4+            {% if boards.has_previous %}
5+                <li class="page-item">
6+                    <a class="page-link" href="?page=1">&laquo; 最初</a>
7+                </li>
8+                <li class="page-item">
9+                    <a class="page-link" href="?page={{ boards.previous_page_number }}">前へ</a>
10+                </li>
11+            {% endif %}
12+
13+            <li class="page-item disabled">
14+                <a class="page-link" href="#">ページ {{ boards.number }} / {{ boards.paginator.num_pages }}</a>
15+            </li>
16+
17+            {% if boards.has_next %}
18+                <li class="page-item">
19+                    <a class="page-link" href="?page={{ boards.next_page_number }}">次へ</a>
20+                </li>
21+                <li class="page-item">
22+                    <a class="page-link" href="?page={{ boards.paginator.num_pages }}">最後 &raquo;</a>
23+                </li>
24+            {% endif %}
25+        </ul>
26+    </section>

ビューから渡されたboardsオブジェクト(Pageオブジェクト)が持つ情報を使い、ページネーションのリンクを動的に生成します。

{% if boards.has_previous %}は、現在表示しているページよりも「前」のページが存在するかどうかを判定するテンプレートタグです。もし前のページが存在する場合(つまり、2ページ目以降を表示している場合)にのみ、{% endif %}までのHTMLが出力されます。このブロックの中では、「最初」ページへのリンク(?page=1)と、「前へ」のリンク(?page={{ boards.previous_page_number }})を生成しています。boards.previous_page_numberで、現在のページの一つ前のページ番号を取得できます。

中央の<li>タグでは、現在のページ情報を表示しています。{{ boards.number }}で現在のページ番号を、{{ boards.paginator.num_pages }}で全体の総ページ数を取得し、「ページ X / Y」という形式で表示します。

{% if boards.has_next %}は、has_previousとは逆に、「次」のページが存在するかどうかを判定します。もし次のページが存在する場合(最後のページ以外を表示している場合)にのみ、{% endif %}までのHTMLが出力されます。このブロックの中では、「次へ」のリンク(?page={{ boards.next_page_number }})と、「最後」のページへのリンク(?page={{ boards.paginator.num_pages }})を生成しています。boards.next_page_numberで次のページの番号を、boards.paginator.num_pagesで最後のページの番号(=総ページ数)を取得できます。

これらのリンクのhref属性が?page=...という形式になっていることで、views.pyrequest.GET.get('page')と連携し、ページを移動する機能が実現されます。

おわりに

今回はDjangoの標準機能であるPaginatorを使い、掲示板にページネーション機能を実装する方法を学びました。ビューでは、Paginatorクラスを使って全投稿データを指定した件数で分割し、URLのパラメータに応じて表示するページを決定しました。テンプレート側では、ビューから渡されたオブジェクトが持つhas_nextnext_page_numberといった属性を利用して、「次へ」やページ番号などのリンクを動的に表示する仕組みを実装しました。

関連コンテンツ