Python + Flask を使ったWebアプリ作成⑦(ページング追加編)

下記記事の続きで、Python Flask を使ったブログサービスの実装を進めていきます。








今回は投稿一覧画面にページング機能を追加します。
投稿が多くなるとスクロールするのが大変になるので、ページング機能を使ってデータを検索できるようにします。


*環境

  • MacOS
  • Python 3.6.3
  • Flask 1.0.2


*参考



*paginate の使い方

Flask-SQLAlchemy では paginate()を使うことで簡単にページング機能を実現することができます。
引数にページあたりの件数を指定することができます。
省略した場合はデフォルト値として 1ページあたり20件表示されるようになります。

ターミナルで仮想環境に入った状態でpythonを入力し、インタラクティブシェルで動作確認をしてみます。
$ python

> from flaskblog.models import Post

# 全件取得
> posts = Post.query.all()
> posts
[Post('My First Blog', '2019-03-24 02:14:39.557748'), Post('Second Blog Title', '2019-03-24 02:29:56.488804'), Post('3rd Blog Title', '2019-03-24 02:31:28.801885')]

for post in posts:
  print(post)

Post('My First Blog', '2019-03-24 02:14:39.557748')
Post('Second Blog Title', '2019-03-24 02:29:56.488804')
Post('3rd Blog Title', '2019-03-24 02:31:28.801885')  


# ページングで取得(デフォルトは1ページあたり20件)
> posts = Post.query.paginate()
> posts

<flask_sqlalchemy.Pagination object at 0x11114ed68>
dir(posts)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'has_next', 'has_prev', 'items', 'iter_pages', 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num', 'query', 'total']

> posts.per_page
20
> posts.page
1

for post in posts.items:
  print(post)

Post('My First Blog', '2019-03-24 02:14:39.557748')
Post('Second Blog Title', '2019-03-24 02:29:56.488804')
Post('3rd Blog Title', '2019-03-24 02:31:28.801885')

# 1ページを取得
posts = Post.query.paginate(page=1)
for post in posts.items:
  print(post)

Post('My First Blog', '2019-03-24 02:14:39.557748')
Post('Second Blog Title', '2019-03-24 02:29:56.488804')
Post('3rd Blog Title', '2019-03-24 02:31:28.801885')

posts.total
3


*ページング機能を追加

ホーム画面の投稿一覧にページング機能を追加します。
初期表示時にデータを取得しているAPIで全件取得している処理をpagenate()を使うように修正します。
requestからページの値を取得する際、指定がなかった場合はデフォルトとして1とするようにしています。
request.args.get(key, default=None, type=None)

<routes.py>
...  
  
@app.route('/')  
@app.route('/home')  
def home():  
    # ----- ↓修正ここから -----
    page = request.args.get('page', 1, type=int)  
    posts = Post.query.order_by(Post.date_posted.desc()).paginate(page=page, per_page=5)  
    # ----- ↑修正ここまで -----
    return render_template('home.html', posts=posts)


ホーム画面にページング機能を追加します。
<templates/home.html>
{% extends "layout.html" %}  
{% block content %}  
    <!-- ↓修正 (itemsを追加) -->
    {% for post in posts.items %}  
    <article class="media content-section">  
        <img class="rounded-circle article-img" src="{{ url_for('static', filename='profile_pics/' + post.author.image_file) }}">  
        <div class="media-body">  
            <div class="article-metadata">  
                <a class="mr-2" href="#">{{ post.author.username }}</a>  
                <small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>  
            </div>  
            <h2><a class="article-title" href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h2>  
            <p class="article-content">{{ post.content }}</p>  
        </div>  
    </article>  
{% endfor %}  
<!-- ↓追加ここから -->
{% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}  
  {% if page_num %}  
    {% if posts.page == page_num %}  
      <a class="btn btn-info mb-4" href="{{ url_for('home', page=page_num) }}">{{ page_num }}</a>  
    {% else %}  
      <a class="btn btn-outline-info mb-4" href="{{ url_for('home', page=page_num) }}">{{ page_num }}</a>  
    {% endif %}  
  {% else %}  
    ...  
  {% endif %}  
{% endfor %}  
<!-- ↑追加ここまで -->
{% endblock content %}


*アカウント毎の投稿を表示

ホーム画面の投稿一覧でアカウントの名前をクリックすると、アカウント毎の投稿を表示できるようにします。
<templates/home.html>
{% extends "layout.html" %}  
{% block content %}  
    {% for post in posts.items %}  
    <article class="media content-section">  
        <img class="rounded-circle article-img" src="{{ url_for('static', filename='profile_pics/' + post.author.image_file) }}">  
        <div class="media-body">  
            <div class="article-metadata">  
                <!-- ↓修正 -->
                <a class="mr-2" href="{{ url_for('user_posts', username=post.author.username) }}">{{ post.author.username }}</a>  
                <small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>  
            </div>  
            <h2><a class="article-title" href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h2>  
            <p class="article-content">{{ post.content }}</p>  
        </div>  
    </article>  
{% endfor %}  
{% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}  
  {% if page_num %}  
    {% if posts.page == page_num %}  
      <a class="btn btn-info mb-4" href="{{ url_for('home', page=page_num) }}">{{ page_num }}</a>  
    {% else %}  
      <a class="btn btn-outline-info mb-4" href="{{ url_for('home', page=page_num) }}">{{ page_num }}</a>  
    {% endif %}  
  {% else %}  
    ...  
  {% endif %}  
{% endfor %}  
{% endblock content %}

アカウント毎の投稿を表示するAPIを追加します。
ページング機能を付けるので、リクエストでユーザー名とページ番号を受け取るようにします。
<routes.py>
...

@app.route('/user/<string:username>')  
def user_posts(username):  
    page = request.args.get('page', 1, type=int)  
    user = User.query.filter_by(username=username).first_or_404()  
    posts = Post.query.filter_by(author=user)\  
        .order_by(Post.date_posted.desc())\  
        .paginate(page=page, per_page=5)  
    return render_template('user_posts.html', posts=posts, user=user)
    

アカウント毎に投稿一覧を表示する画面を新規作成します。
<templates/user_posts.html>
{% extends "layout.html" %}  
{% block content %}  
    <h1 class="mb-3">Posts by {{ user.username }} ({{ posts.total }})</h1>  
    {% for post in posts.items %}  
    <article class="media content-section">  
        <img class="rounded-circle article-img" src="{{ url_for('static', filename='profile_pics/' + post.author.image_file) }}">  
        <div class="media-body">  
            <div class="article-metadata">  
                <a class="mr-2" href="{{ url_for('user_posts', username=post.author.username) }}">{{ post.author.username }}</a>  
                <small class="text-muted">{{ post.date_posted.strftime('%Y-%m-%d') }}</small>  
            </div>  
            <h2><a class="article-title" href="{{ url_for('post', post_id=post.id) }}">{{ post.title }}</a></h2>  
            <p class="article-content">{{ post.content }}</p>  
        </div>  
    </article>  
    {% endfor %}  
    {% for page_num in posts.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %}  
        {% if page_num %}  
            {% if posts.page == page_num %}  
                <a class="btn btn-info mb-4" href="{{ url_for('user_posts', username=user.username, page=page_num) }}">{{ page_num }}</a>  
            {% else %}  
                <a class="btn btn-outline-info mb-4" href="{{ url_for('user_posts', username=user.username, page=page_num) }}">{{ page_num }}</a>  
            {% endif %}  
        {% else %}  
        ...  
        {% endif %}  
    {% endfor %}  
{% endblock content %}


*動作確認

サーバーを起動して画面を確認します。
$ python run.py

一覧画面の下部分にページングが追加され、投稿が5件ずつ表示されます。











2ページ目をクリックすると、過去に投稿された1件が表示されます。








さらに投稿のアカウント名のリンクをクリックすると、そのアカウント毎の一覧が表示されます。









アカウント毎の表示でBobのときは4件表示されます。












*所感

ページング機能の処理を初めて実装したのですが、画面だけの修正だけでなくDBの取得方法から修正しなければいけないので、少し手間がかかることを実感しました。
Flask-SQLAlchemy でpagenate()があったので実装が簡単でしたが、なかった場合は自分でページ毎に件数を取得しなければならないので大変そうです。(Djangoでもページング機能のメソッドがあるみたいなので大体は自分で実装することはなさそうです)

Previous
Next Post »

人気の投稿