ミゲル・グリンバーグ
これは、Mega-Tutorial Flaskシリーズの第9版で、データベース内のリストを分割する方法を説明します。
参考までに、以下はこのシリーズの記事のリストです。
- 第1章:Hello world!
- 第2章:テンプレート
- 第3章:Webフォーム
- 第4章:データベース
- 第5章:ユーザーログイン
- 第6章:プロフィールページとアバター
- 第7章:エラー処理
- 第8章:サブスクライバー、連絡先、およびフレンド
- 第9章:ページネーション (この記事)
- 第10章:メールサポート
- 第11章:再構成
- 第12章:日付と時刻
- 第14章:Ajax
- 第15章:アプリケーション構造の改善
- 第15章:アプリケーション構造の改善
- 第16章:全文検索
- 第17章:Linuxの展開
- 第18章:Herokuでの展開
- 第19章:Dockerコンテナーでの展開
- 第20章:JavaScriptマジック
- 第21章:ユーザー通知
- 第22章:バックグラウンドタスク
- 第23章:アプリケーションプログラミングインターフェイス(API)
注1:このコースの古いバージョンをお探しの場合は、こちらをご覧ください 。
注2:突然、このブログの私の(ミゲル)の仕事を支持して声を上げたい場合、または単に記事を1週間待つ忍耐がない場合、私(ミゲルグリーンバーグ)はこのガイドの完全版にパッケージ化された電子書籍またはビデオを提供します。 詳細については、 learn.miguelgrinberg.comをご覧ください 。
第8章では、ソーシャルネットワークで非常に人気のある「フォロワー」(サブスクライバー)パラダイムをサポートするために必要なデータベースにいくつかの変更を加えました。 この機能により、デモンストレーション用に作成した最後のエントリを削除する準備ができました。 これらは偽のメッセージです。
この章では、アプリケーションはユーザーからのブログ投稿の受信を開始し、それらをインデックスページとプロファイルページに配信します。
この章のGitHubリンク: Browse 、 Zip 、 Diff 。
ブログ投稿
簡単なものから始めましょう。 ホームページには、ユーザーが新しいメッセージを入力できるフォームが必要です。 最初にフォームクラスを作成します。
class PostForm(FlaskForm): post = TextAreaField('Say something', validators=[ DataRequired(), Length(min=1, max=140)]) submit = SubmitField('Submit')
これで、このフォームをアプリケーションのメインページのテンプレートに追加できます。
{% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> <form action="" method="post"> {{ form.hidden_tag() }} <p> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4) }}<br> {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </p> <p>{{ form.submit() }}</p> </form> {% for post in posts %} <p> {{ post.author.username }} says: <b>{{ post.body }}</b> </p> {% endfor %} {% endblock %}
このテンプレートへの変更は、他のフォームで行われた変更に似ています。 結論として、フォームの作成と処理を表示機能に追加する必要があります。
from app.forms import PostForm from app.models import Post @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = PostForm() if form.validate_on_submit(): post = Post(body=form.post.data, author=current_user) db.session.add(post) db.session.commit() flash('Your post is now live!') return redirect(url_for('index')) posts = [ { 'author': {'username': 'John'}, 'body': 'Beautiful day in Portland!' }, { 'author': {'username': 'Susan'}, 'body': 'The Avengers movie was so cool!' } ] return render_template("index.html", title='Home Page', form=form, posts=posts)
このビュー関数への変更を順番に見てみましょう。
- 追加された新しいインポート:
Post
およびPostForm
- このビュー関数はフォームデータを受け取るようになったため、
GET
要求に加えて、index
ページビュー関数に関連付けられた両方のルートでのPOST
要求。 - フォーム処理ロジックは、新しい
Post
エントリをデータベースに追加します。 - テンプレートは、テキストオブジェクトを表示できるように、オプションの引数として
form
オブジェクトを受け取ります。
続行する前に、Webフォームの処理に関連するいくつかの重要なポイントに注意を喚起したいと思います。 フォームデータを処理した後、 インデックスのメインページにリダイレクトを送信してリクエストを終了することに注意してください。 これは既にインデックスビュー関数であるため、リダイレクトを簡単にスキップして、関数をテンプレートレンダリングパーツで引き続き機能させることができます。
なぜリダイレクトするのですか?
標準的な方法は、リダイレクトされたWebフォームを送信するときにPOST
要求の要求に応答することです。 これは、Webブラウザーで更新コマンドを使用するときに、刺激の発作を何らかの形で回避するのに役立ちます。 結局、更新ボタンをクリックすると、Webブラウザに最後のリクエストが表示されます。 フォーム送信を伴うPOST
リクエストが通常の応答を返す場合、更新はフォームを再送信します。 これは常に予期しないことであるため、ブラウザーはユーザーに再送信の確認を求めますが、ほとんどのユーザーはブラウザーに必要なものを理解しません。
しかし、リダイレクトがPOST
リクエストに応答すると、ブラウザはGET
リクエストを送信してリダイレクトで指定されたページをキャプチャするように指示されるため、最後のリクエストはPOSTリクエストではなくなり、更新コマンドはより予測可能な方法で機能します。
この単純なトリックは、 Post / Redirect / Getパターンにすぎません。 。 ユーザーがWebフォームを送信した後に誤ってページを更新したときに、重複したメッセージが挿入されるのを防ぎます。
ブログ投稿を見る
覚えているなら、私は長い間ホームページに表示しているいくつかのブログ投稿を作成しました。 これらの偽オブジェクトは、単純なPythonリストとしてインデックスビュー関数で明示的に作成されます。
posts = [ { 'author': {'username': 'John'}, 'body': 'Beautiful day in Portland!' }, { 'author': {'username': 'Susan'}, 'body': 'The Avengers movie was so cool!' } ]
しかし今、私はUser
モデルにfollowed_posts()
メソッドがあり、このユーザーが見たいメッセージを返します。 そのため、一時メッセージを実際のメッセージに置き換えることができます。
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): # ... posts = current_user.followed_posts().all() return render_template("index.html", title='Home Page', form=form, posts=posts)
User
クラスのfollowed_posts
メソッドは、 User
がデータベースから購読したメッセージをキャプチャするように構成されたSQLAlchemyクエリオブジェクトを返します。 このリクエストでall()
を呼び出すと実行が開始され、戻り値はすべての結果を含むリストになります。
したがって、これまでに使用した一時メッセージを生成したものと非常によく似た構造を取得します。 これは非常に似ているため、テンプレートを変更する必要さえありません。
ユーザー検索を促進する
現時点でアプリケーションがどのように機能するかは、ユーザーが他のユーザーを見つけられるようにするのにあまり便利ではないことに気づいたことを願っています。 実際、他のユーザーが何をしているのかを確認する方法はまったくありません。 いくつかの簡単な変更でこれを修正します。
新しいページを作成する必要があります。これを「探索」ページと呼びます。 このページはホームページとして機能しますが、次のユーザーからのメッセージのみを表示する代わりに、すべてのユーザーからのグローバルなメッセージフローを表示します。 新しいビュー関数は次のとおりです。
@app.route('/explore') @login_required def explore(): posts = Post.query.order_by(Post.timestamp.desc()).all() return render_template('index.html', title='Explore', posts=posts)
この機能に奇妙なことに気づいたことがありますか? render_template()
の呼び出しは、アプリケーションのメインページで使用するindex.htmlテンプレートを参照します。 このページはメインページと非常に似ているため、テンプレートを再利用することにしました。
ただし、メインページとの1つの違いは、[探索]ページにはブログ投稿を書くためのフォームが必要ないため、このビュー関数ではテンプレート呼び出しにform
引数を含めなかったことです。
index.htmlテンプレートがクラッシュしないように、存在しないWebフォームを表示しようとすると、定義されている場合にのみフォームを表示する条件を追加します。
{% extends "base.html" %} {% block content %} <h1>Hi, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post"> ... </form> {% endif %} ... {% endblock %}
また、ナビゲーションバーにこの新しいページへのリンクを追加します。
<a href="{{ url_for('explore') }}">Explore</a>
ユーザープロファイルページにブログ投稿を表示するには、 第6章の_post.html
サブパターンを覚えていますか? これは、ユーザープロファイルページテンプレートから抽出され、他のテンプレートから使用できるように分離された小さなテンプレートです。 ブログ投稿のユーザー名をリンクとして表示できるように、少し改善します。
<table> <tr valign="top"> <td><img src="{{ post.author.avatar(36) }}"></td> <td> <a href="{{ url_for('user', username=post.author.username) }}"> {{ post.author.username }} </a> says:<br>{{ post.body }} </td> </tr> </table>
これで、このサブテンプレートを使用して、ホームページでブログを視覚化および学習できます。
... {% for post in posts %} {% include '_post.html' %} {% endfor %} ...
ネストされたテンプレートは、 post
という名前の変数が存在することを想定しており、それがインデックステンプレート内のループ変数が呼び出されるため、これは正常に機能します。
これらの小さな変更のおかげで、アプリケーションの使いやすさが大幅に向上しました。 これで、ユーザーはページにアクセスして未知のユーザーからのブログ投稿を読むことができ、これらの投稿に基づいて新しいユーザーを見つけてサブスクリプションを追加できます。 すごいね
この時点で、アプリケーションを再試行して、これらの最新のUIの改善を体験することをお勧めします。
ブログ投稿の共有
アプリケーションは以前よりも見栄えがよくなりますが、ホームページにすべてのエントリを表示することは、想像よりもはるかに早く問題になります。 ユーザーが1,000件のレコードを持っている場合はどうなりますか? それとも100万? このような大量のメッセージのリストの管理は、非常に遅く、非効率的です。
この問題を解決するために、メッセージのリストを分割します。 つまり、最初は一度に限られた数のメッセージのみを表示し、メッセージリストの残りの部分をナビゲートするリンクを含めます。 Flask-SQLAlchemyは、 paginate()
クエリメソッドを使用してネイティブにページネーションをサポートします。 たとえば、最初の20個のユーザーレコードを取得する必要がある場合、リクエストの最後にall()
への呼び出しを置き換えることができます。
>>> user.followed_posts().paginate(1, 20, False).items
paginate
メソッドは、Flask-SQLAlchemyの任意のクエリオブジェクトに対して呼び出すことができます。 これには3つの引数が必要です。
- 1から始まるページ番号
- ページあたりの要素数
- エラーフラグ。
True
場合、範囲外のページが要求されると、404エラーが自動的にクライアントに返されます。False
場合、範囲外のページに対して空のリストが返されます。
paginate
からの戻り値は、 Pagination
オブジェクトです。 このオブジェクトのitems
属性には、リクエストされたページのアイテムのリストが含まれています。 Paginationオブジェクトにはまだ有用なものがありますが、これについては後で説明します。
次に、 index()
ビュー関数でページネーションを実装する方法について考えてみましょう。 まず、ページに表示されるアイテムの数を決定する構成アイテムをアプリケーションに追加します。
class Config(object): # ... POSTS_PER_PAGE = 3
アプリケーション全体のこれらの「ノブ」が構成ファイルの動作に影響を与える可能性があることをお勧めします。これにより、1か所ですべての修正を行うことができます。 その結果、私はもちろん、ページ上で3つ以上の要素を使用しますが、テストには小さな数で作業するのが便利です。
次に、ページ番号をアプリケーションURLに含める方法を決定する必要があります。 かなり一般的な方法は、 クエリ文字列引数を使用してオプションのページ番号を指定することです。指定されていない場合は、デフォルトでページ1に表示されます。 これを実装する方法を示すURLの例を次に示します。
- ページ1、暗黙: http:// localhost:5000 /インデックス
- ページ1、明示的: http:// localhost:5000 /インデックス?ページ= 1
- ページ3: http:// localhost:5000 /インデックス?ページ= 3
クエリ文字列で指定された引数にアクセスするには、Flaskオブジェクトのrequest.args
オブジェクトを使用できます。 これについては、 第5章ですでに説明しました。ここでは、Flask-Loginからのユーザーログイン用のURLを実装しました。
次の例は、ホームページの内訳をいくつかに追加し、表示機能を調査した方法を示しています。
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): # ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template('index.html', title='Home', form=form, posts=posts.items) @app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) return render_template("index.html", title='Explore', posts=posts.items)
これらの変更では、2つのルートの両方が、 page
リクエストのpage
引数から、またはデフォルトで1のいずれかで、表示するページ番号を決定します。次に、 paginate()
メソッドを使用して、結果の目的のページのみを取得します。 ページサイズを決定するPOSTS_PER_PAGE
構成POSTS_PER_PAGE
、 POSTS_PER_PAGE
オブジェクトからアクセスできます。
これらの変更がどれほど簡単で、どのコードにもほとんど影響しないことに注意してください。 私は各部分を書き、他の部分の作業から抽象化しようとしています。これにより、拡張とテストが容易なモジュール式で信頼性の高いアプリケーションを作成できます。 同時に、致命的なミスや小さなミスが発生する可能性は本質的に小さいです。
続けて! そして、ページネーション機能を試してみるべきです。 事前に3つ以上のブログ投稿があることを確認してください。 これは、すべてのユーザーからのメッセージが表示される検索ページで見やすくなります。 これで、最後の3つのメッセージのみを表示できます。 次の3つを表示する場合は、 http://localhost:5000/explore?page=2
をブラウザーのアドレスバーに入力します。
ページナビゲーション
次の変更点は、ブログ投稿のリストの下部にリンクを追加して、ユーザーが次のページや前のページに移動できるようにすることです。 paginate()
呼び出しからの戻り値は、Flask-SQLAlchemyからのPaginationクラスのオブジェクトであることを述べたことを思い出してください。 これまで、このオブジェクトのitems属性を使用しましたが、
選択したページについて取得されたアイテムのリストが含まれます。 しかし、このオブジェクトには、ページへのリンクを作成するときに役立ついくつかの他の属性があります。
- has_next:現在のページの後に少なくとも1つのページがある場合はtrue
- has_prev:現在のページの前に別のページがある場合はtrue
- next_num:次のページのページ番号
- prev_num:前のページのページ番号
これら4つの要素を使用して、ページ(次および前)へのリンクを作成し、それらを表示用のテンプレートに渡すことができます。
@app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): # ... page = request.args.get('page', 1, type=int) posts = current_user.followed_posts().paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('index', page=posts.next_num) \ if posts.has_next else None prev_url = url_for('index', page=posts.prev_num) \ if posts.has_prev else None return render_template('index.html', title='Home', form=form, posts=posts.items, next_url=next_url, prev_url=prev_url) @app.route('/explore') @login_required def explore(): page = request.args.get('page', 1, type=int) posts = Post.query.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('explore', page=posts.next_num) \ if posts.has_next else None prev_url = url_for('explore', page=posts.prev_num) \ if posts.has_prev else None return render_template("index.html", title='Explore', posts=posts.items, next_url=next_url, prev_url=prev_url)
これら2つの関数のnext_url
およびprev_url
は、その方向にページがある場合にのみ、 url_for()
によって返されるURLを使用します。 現在のページがメッセージコレクションの一方の端にある場合、 has_next
オブジェクトのhas_next
またはhas_prev
属性はFalse
になります。この場合、この方向のリンクはNone
に設定されます。
前に省略したurl_for()
関数の興味深い側面の1つは、キーワード引数を追加できることです。これらの引数の名前がURLで直接指定されていない場合、FlaskはそれらをURLに含めます要求引数としてのアドレス。
ページへのリンクはindex.htmlテンプレートで設定されているので、投稿のリストのすぐ下のページにリンクを表示しましょう。
... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %} ...
このアドオンは、インデックスのメインページと調査中のページの両方の投稿のリストの下にリンクを追加します。 最初のリンクは「新しい投稿」としてマークされ、前のページを指します(最新のデータでソートされたメッセージを表示しているため、最初のページは最新のコンテンツです)。
2番目のリンクは「古い投稿」としてマークされ、投稿の次のページを指します。
これら2つのリンクのいずれかがNone
場合、リンクは条件式を介してページに表示されません。
ユーザープロファイルページの改ページ
現在、インデックスページに十分な変更があります。 ただし、ユーザープロファイルページには、プロファイル所有者からのメッセージのみを表示するメッセージのリストも含める必要があります。 一貫性を保つには、ユーザープロファイルページをインデックスページと同様に変更する必要があります。
一時メッセージのリストがまだあるユーザープロファイルビュー機能を更新することから始めます。
@app.route('/user/<username>') @login_required def user(username): user = User.query.filter_by(username=username).first_or_404() page = request.args.get('page', 1, type=int) posts = user.posts.order_by(Post.timestamp.desc()).paginate( page, app.config['POSTS_PER_PAGE'], False) next_url = url_for('user', username=user.username, page=posts.next_num) \ if posts.has_next else None prev_url = url_for('user', username=user.username, page=posts.prev_num) \ if posts.has_prev else None return render_template('user.html', user=user, posts=posts.items, next_url=next_url, prev_url=prev_url)
ユーザーからメッセージのリストを取得するには、 user.posts
関係が、ユーザーモデルのdb.relationship()
定義の結果としてSQLAlchemyが既に構成したクエリであるという事実を使用します。 このクエリを使用し、 order_by()
を追加して最新の投稿を最初に取得してから、indexおよびexploreの投稿とまったく同じページ分割を行います。 url_for()
によって生成されたページへのリンクは、URLの動的コンポーネントとしてこのユーザー名を持つユーザープロファイルページを指すため、追加のusername
引数が必要であることに注意してください。
結論として、 user.htmlテンプレートへの変更は、インデックスページで行った変更と同じです。
... {% for post in posts %} {% include '_post.html' %} {% endfor %} {% if prev_url %} <a href="{{ prev_url }}">Newer posts</a> {% endif %} {% if next_url %} <a href="{{ next_url }}">Older posts</a> {% endif %}
ページネーション機能の実験が終了したら、構成POSTS_PER_PAGE
より適切な値に設定できます。
class Config(object): # ... POSTS_PER_PAGE = 25