Djangoで、Paginatorやdjango-pure-paginationを使ってページングしてみた

Djnagoで、1ページあたりの件数が多くなった時にページングする方法を調べた時のメモです。

なお、ページングという用語については、

  • ページング(Paging)
  • ページネーション(Pagination)
  • ページャー(Pager)

などの呼び方があるものの、今回は以下に従いページングと呼ぶことにします。
一覧リストにおけるページングについて : circumstance evidence

 

環境

 
2018/8/26 追記

この記事で取り上げた django-pure-pagination ですが、

  • 現在メンテナンスされていない
  • Django2.1以降に対応していない

の問題があるようです。

そのため、新規に導入する場合には、別のライブラリを使ったほうが良いです。

2018/8/26 追記 ここまで

 

作成するページングのタイプ

現在のページと、最大ページ数を表示するタイプ

Djangoドキュメントのサンプルのようなタイプです。
Pagination - Using Paginator in a view | Django documentation | Django

f:id:thinkAmi:20160204061437p:plain

 

ページ番号をすべて表示するタイプ

f:id:thinkAmi:20160204061458p:plain

 

ページ数が多い場合に、ページ番号の一部を省略するタイプ

Django Admin siteのようなものです。

f:id:thinkAmi:20160204061532p:plain

 

準備

アプリ作成まで
d:\Sandbox\django_pagination_sample>virtualenv -p c:\python35-32\python.exe env
d:\Sandbox\django_pagination_sample>env\Scripts\activate
(env) d:\Sandbox\django_pagination_sample>pip install django

(env) d:\Sandbox\django_pagination_sample>django-admin startproject myproject .
(env) d:\Sandbox\django_pagination_sample>python manage.py startapp myapp

 

settings.py

INSTALLED_APPSにmyappアプリを追加します。

INSTALLED_APPS = [
    ...
    'myapp',
]

 

myapp/models.py

ListViewに表示するModelを作成します。

from django.db import models

class Article(models.Model):
    title = models.CharField('title', max_length=255)
    content = models.CharField('content', max_length=255)

 

myapp/fixtures/initial_data.json

ページングできるよう、15件ほどのデータをfixtureとして用意しておきます。

[
    {
        "model": "myapp.Article",
        "pk": 1,
        "fields": {
            "title": "title-1",
            "content": "content-1"
        }
    },
    ...
]

 

myproject/urls.py

Djangoアプリのurlsをincludeし、example.com/mysite/<DjangoアプリのurlpatternsによるURL>という形のURLにします。

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    ...
    url(r'^mysite/', include('myapp.urls', namespace='my')),
]

 

マイグレーションとデータ投入

あとはマイグレーションとデータ投入を行うことで、準備ができました。

(env) d:\Sandbox\django_pagination_sample>python manage.py makemigrations
(env) d:\Sandbox\django_pagination_sample>python manage.py migrate
(env) d:\Sandbox\django_pagination_sample>python manage.py loaddata initial_data

 

現在のページと、最大ページ数を表示するタイプ

Djangoドキュメントのサンプル通りに実装してみます。

 

myapp/views.py

今回はclass-based viewのListViewを使います。クラスメンバpaginate_byで1ページあたりの件数を指定します。

class PrevNextPaginatorView(ListView):
    model = models.Article
    template_name = 'myapp/prev_next.html'
    paginate_by = 1

 

myapp/urls.py

名前付URLパターンを指定します。

urlpatterns = [
    url(r'^prev-next/$', views.PrevNextPaginatorView.as_view(), name='prev_next'),
]

 

myapp/templates/myapp/prev_next.html

表示するテンプレートを作成します。

プロジェクトのurls.pyでアプリのurls.pyをincludeしているため、example.com/prev-next/?page=ではなくexample.com/mysite/prev-next/?page=とする必要があります。

そのため、テンプレートタグのurl + 名前付URLパターンを使います。
Built-in template tags and filters - url | Django documentation | Django

{% if is_paginated %}
    <div class="pagination">
        <span class="step-link">
            {% if page_obj.has_previous %}
                <a href="{% url 'my:prev_next' %}?page={{ page_obj.previous_page_number }}">previous</a>
            {% endif %}
            <span class="page-current">
                Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
            </span>
            {% if page_obj.has_next %}
                <a href="{% url 'my:prev_next' %}?page={{ page_obj.next_page_number }}">next</a>
            {% endif %}
        </span>
    </div>
{% endif %}

 

動作確認
1ページ目

f:id:thinkAmi:20160204061842p:plain

2ページ目以降

f:id:thinkAmi:20160204061853p:plain

 

Modelのpkの降順でページング

デフォルトではModelのpkの昇順での表示となります。

pkの降順とするために、今回はDjango1.8よりViewに追加されたorderingを使ってみます。
Multiple object mixins | Django documentation | Django

class PrevNextPaginatorByDescView(ListView):
    model = models.Article
    template_name = 'myapp/prev_next_desc.html'
    paginate_by = 1
    ordering = '-pk'

 
あとはテンプレートとurls.pyに設定することで、以下のように降順で表示できます。

f:id:thinkAmi:20160204061922p:plain

 

ページ番号をすべて表示するタイプ

以下を参考に、ページ番号をすべて表示するページングを作成します。
Python Django入門 (5) - Qiita

必要なものは

  • myapp/views.py
  • myapp/urls.py
  • myapp/templates/myapp/all.html
  • myapp/static/myapp/css/pagination.css
    • ページングで<ul>タグを使っていることから、CSSのinlineを使うため

ですが、viewsとurlsは特に変更がないため、ここでの記載は省略します。

 

myapp/templates/myapp/all.html

if文を書きたい時のテンプレートタグとして、

  • if hoge == fugaを使う
  • ifequal hoge fugaを使う

どちらを使うのが良いのかを調べてみたところ、公式ドキュメントに

The ifequal and ifnotequal tags will be deprecated in a future release.

Built-in template tags and filters - ifequal and ifnotequal | Django documentation | Django

との記載があったため、今回はifを使うようにしました。

また、staticを使って、CSSファイルを読み込むようにします。

<head>
    ...
    {% load staticfiles %}
    <link href={% static 'myapp/css/pagination.css' %} rel='stylesheet'>
</head>
<body>
    ...
    {% if is_paginated %}
        <ul class="pagination">
            {% if page_obj.has_previous %}
                <li><a href="{% url 'my:all' %}?page={{ page_obj.previous_page_number }}">previous</a></li>
            {% endif %}
            {% for link_page in page_obj.paginator.page_range %}
                {% if link_page == page_obj.number %}
                    <li class="active">{{ link_page }}</li>
                {% else %}
                    <li><a href="{% url 'my:all' %}?page={{ link_page }}">{{ link_page }}</a></li>
                {% endif %}
            {% endfor %}

            {% if page_obj.has_next %}
                <li><a href="{% url 'my:all' %}?page={{ page_obj.next_page_number }}">next</a></li>
            {% endif %}
        </ul>
    {% endif %}
</body>

 

myapp/static/myapp/css/pagination.css
ul.pagination li {
    display: inline
}

 

動作確認

f:id:thinkAmi:20160204062117p:plain

 

ページ数が多い場合に、ページ番号の一部を省略するタイプ

Admin siteのページングを確認

DjangoのAdmin siteのページング方法が気になったため、Admin siteでArticleを表示できるようにします。

なお、Admin siteの1ページあたりの件数はデフォルトが100件のため、1ページ1件で表示できるように変更します。
Django admin pagination question - Stack Overflow

myapp/admin.py
from django.contrib import admin
from . import models

class ArticleAdmin(admin.ModelAdmin):
    list_per_page = 1
    
admin.site.register(models.Article, ArticleAdmin)

 
Admin siteにログインするため、superuserも作っておきます。

(env) d:\Sandbox\django_pagination_sample>python manage.py createsuperuser --username=admin --email=admin@example.com
Password:
Password (again):
Superuser created successfully.

 
Admin siteにログインして動作確認しました。

f:id:thinkAmi:20160204062126p:plain

今回のアプリもこんな感じの表示を目指します。

 

実装方法の調査

自作する方法は以下が参考になりましたが、いろいろと手間そうでした。

 
一方、Djangoパッケージを調べてみたところ、いくつかありました。
Django Packages : Pagination

その中でも、django-paginationを使う記事を見かけたものの、現在ではメンテナンスされてなさそうでした。
django-pagination 1.0.7 : Python Package Index

そのため、

  • Python3に対応
  • 最近メンテナンスされているもの

という点から、django-pure-paginationを試してみることにました。 jamespacileo/django-pure-pagination - Python

 
2018/8/26 追記

再掲となりますが django-pure-pagination

  • 現在メンテナンスされていない
  • Django2.1以降に対応していない

の問題があるようです。

そのため、新規に導入する場合には、別のライブラリを使ったほうが良いです。

2018/8/26 追記 ここまで

 

django-pure-paginationを試す

pipでインストールします。

(env) d:\Sandbox\django_pagination_sample>pip install django-pure-pagination

 
django-pure-paginationのREADMEに従い、必要なファイルに追記します。

myproject/settings.py

INSTALLED_APPSPAGINATION_SETTINGSの設定を行います。

INSTALLED_APPS = [
    ...
    'myapp',
    'pure_pagination',  # for django-pure-pagination
]
...
# for django-pure-pagination settings
PAGINATION_SETTINGS = {
    'PAGE_RANGE_DISPLAYED': 2,
    'MARGIN_PAGES_DISPLAYED': 2,

    'SHOW_FIRST_PAGE_WHEN_INVALID': True,
}

 

myapp/views.py

PaginationMixinを使ったViewを作成します。

from django.views.generic import ListView
from pure_pagination.mixins import PaginationMixin
from . import models
...
class PurePaginatorView(PaginationMixin, ListView):
    model = models.Article
    template_name = 'myapp/pure.html'
    paginate_by = 1

 

myapp/urls.py
urlpatterns = [
    ...
    url(r'^pure/$', views.PurePaginatorView.as_view(), name='pure'),
]

 

myapp/templates/myapp/pure.html

README通りだとどこが省略された部分を表示しているのか分かりづらかったため、(省略)という文字を入れるようにしました。

<head>
    ...
    {% load staticfiles %}
    <link href={% static 'myapp/css/pagination.css' %} rel='stylesheet'>
</head>
<body>
    ...
    {% if is_paginated %}
        <ul class="pagination">
            {% if page_obj.has_previous %}
                <li><a href="{% url 'my:pure' %}?{{ page_obj.previous_page_number.querystring }}">previous</a></li>
            {% endif %}
            {% for link_page in page_obj.pages %}
                {% if link_page %}
                    {% if link_page == page_obj.number %}
                        <li class="active">{{ link_page }}</li>
                    {% else %}
                        <li><a href="{% url 'my:pure' %}?{{ link_page.querystring  }}">{{ link_page }}</a></li>
                    {% endif %}
                {% else %}
                    {# 省略された部分の表示内容 #}
                    <li>(省略)</li>
                {% endif %}
            {% endfor %}

            {% if page_obj.has_next %}
                <li><a href="{% url 'my:pure' %}?{{ page_obj.next_page_number.querystring }}">next</a></li>
            {% endif %}
        </ul>
    {% endif %}
</body>

 

動作確認
1ページ目の表示

f:id:thinkAmi:20160204062352p:plain

 

省略された時の表示

f:id:thinkAmi:20160204062446p:plain

 

django-pure-paginationとBootstrapを組み合わせる

BootstrapにはPaginationのコンポーネントがありますので、それを組み合わせてみます。

といってもそれほど修正する部分は多くなく、上記のテンプレートに、

  • Bootstrap用のCSSJavaScriptを組み込む (今回はCDNを使用)
  • <li>タグにclassを加える
  • <li>タグの中に<a>タグを用意する

を加えるだけでBootstrapでの表示ができます。

myapp/templates/myapp/bootstrap_pure.html
<!DOCTYPE html>
<html lang="ja" xmlns="http://www.w3.org/1999/xhtml">
<head>
    ...
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha256-KXn5puMvxCw+dAYznun+drMdG1IFl3agK0p/pqT9KAo= sha512-2e8qq0ETcfWRI4HJBzQiA3UoyFk6tbNyG+qSaIBZLyW9Xf3sWZHN/lxe9fTh1U45DpPf07yj94KsUHHWe4Yk1A==" crossorigin="anonymous"></script>
</head>
<body>
    ...
    {% if is_paginated %}
        <ul class="pagination">
            {% if page_obj.has_previous %}
                <li><a href="{% url 'my:bootstrap_pure' %}?{{ page_obj.previous_page_number.querystring }}">previous</a></li>
            {% endif %}
            {% for link_page in page_obj.pages %}
                {% if link_page %}
                    {% if link_page == page_obj.number %}
                        <li class="active"><a href="">{{ link_page }}</a></li>
                    {% else %}
                        <li><a href="{% url 'my:bootstrap_pure' %}?{{ link_page.querystring  }}">{{ link_page }}</a></li>
                    {% endif %}
                {% else %}
                    {# 省略された部分の表示内容 #}
                    <li class="disabled"><a href="#">(省略)</a></li>
                {% endif %}
            {% endfor %}

            {% if page_obj.has_next %}
                <li><a href="{% url 'my:bootstrap_pure' %}?{{ page_obj.next_page_number.querystring }}">next</a></li>
            {% endif %}
        </ul>
    {% endif %}
</body>
</html>

 

動作確認

f:id:thinkAmi:20160204221912p:plain

 

ページング部分の共用

以下を参考に、ページング部分を別テンプレートとして切り出して、再利用できるようにしてみます。
Django のページネイトを使い回しできそうな感じにしてみるメモ - 牌語備忘録 -pygo

 
共用するために、

  • ページング部分のテンプレート (share_include.html)
  • ページング部分のテンプレートをincludeするテンプレート (share_base.html)

の2つを作成します。

 

myapp/templates/myapp/share_include.html

Bootstrapで使ったテンプレートを流用し、<a>タグの中身を

  • includeする側で引数url_paramに、名前付きURLパラメータを渡してもらう
  • テンプレートタグのurlにて、url_paramを使う

という内容で修正します。

{% if is_paginated %}
    <ul class="pagination">
        {% if page_obj.has_previous %}
            <li><a href="{% url url_param %}?{{ page_obj.previous_page_number.querystring }}">previous</a></li>
        {% endif %}
        ...
    </ul>
{% endif %}

 

myapp/templates/myapp/share_base.html

共用ページング部分を入れたい場所で、テンプレートタグのincludeを使います。

また、withを使って名前付URLパラメータを渡します。
Built-in template tags and filters - include | Django documentation | Django

今回は、tableの上下に共用のページングを表示してみます。

...
<div id="main">
    <h1>Shared pagination Page</h1>
    
    {% include "myapp/share_include.html" with url_param="my:share" %}    
    
    <table>
        ...
    </table>
    
    {% include "myapp/share_include.html" with url_param="my:share" %}
</div>
... 

 

動作確認

f:id:thinkAmi:20160204225444p:plain

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_pagination_sample