本文翻譯自The Flask Mega-Tutorial Part IX: Paginationhtml
這是Flask Mega-Tutorial系列的第九部分,我將告訴你如何對數據列表進行分頁。git
在第八章我已經作了幾個數據庫更改,以支持在社交網絡很是流行的「粉絲」機制。 有了這個功能,接下來我準備好刪除一開始就使用的模擬用戶動態了。 在本章中,應用將開始接受來自用戶的動態更新,並將其發佈到網站首頁和我的主頁。github
本章的GitHub連接爲:Browse, Zip, Diff.數據庫
讓咱們從簡單的事情開始吧。 首頁須要有一個表單,用戶能夠在其中鍵入新動態。 我建立一個表單類:flask
app / forms.py:博客提交表單。
1 class PostForm(FlaskForm): 2 post = TextAreaField('Say something', validators=[ 3 DataRequired(), Length(min=1, max=140)]) 4 submit = SubmitField('Submit')
而後,我將該表單添加到網站首頁的模板中:瀏覽器
app / templates / index.html:索引模板中的提交表單
1 {% extends "base.html" %} 2 3 {% block content %} 4 <h1>Hi, {{ current_user.username }}!</h1> 5 <form action="" method="post"> 6 {{ form.hidden_tag() }} 7 <p> 8 {{ form.post.label }}<br> 9 {{ form.post(cols=32, rows=4) }}<br> 10 {% for error in form.post.errors %} 11 <span style="color: red;">[{{ error }}]</span> 12 {% endfor %} 13 </p> 14 <p>{{ form.submit() }}</p> 15 </form> 16 {% for post in posts %} 17 <p> 18 {{ post.author.username }} says: <b>{{ post.body }}</b> 19 </p> 20 {% endfor %} 21 {% endblock %}
模板中的變動和處理之前的表單相似。最後的部分是將表單處理邏輯添加到視圖函數中:網絡
app / routes.py:在索引視圖功能中發佈提交表單。
1 from app.forms import PostForm 2 from app.models import Post 3 4 @app.route('/', methods=['GET', 'POST']) 5 @app.route('/index', methods=['GET', 'POST']) 6 @login_required 7 def index(): 8 form = PostForm() 9 if form.validate_on_submit(): 10 post = Post(body=form.post.data, author=current_user) 11 db.session.add(post) 12 db.session.commit() 13 flash('Your post is now live!') 14 return redirect(url_for('index')) 15 posts = [ 16 { 17 'author': {'username': 'John'}, 18 'body': 'Beautiful day in Portland!' 19 }, 20 { 21 'author': {'username': 'Susan'}, 22 'body': 'The Avengers movie was so cool!' 23 } 24 ] 25 return render_template("index.html", title='Home Page', form=form, 26 posts=posts)
咱們來一個個地解讀該視圖函數的變動:session
Post
和PostForm
類index
視圖函數的兩個路由都新增接受POST
請求,以便視圖函數處理接收的表單數據post
表插入一條新的數據form
對象,以便渲染文本輸入框在繼續以前,我想提一些與Web表單處理相關的重要內容。 請注意,在處理表單數據後,我經過發送重定向到主頁來結束請求。 我能夠輕鬆地跳太重定向,並容許函數繼續向下進入模板渲染部分,由於這已是主頁視圖函數了。app
那麼,爲何重定向呢? 經過重定向來響應Web表單提交產生的POST請求是一種標準作法。 這有助於緩解在Web瀏覽器中執行刷新命令的煩惱。 當你點擊刷新鍵時,全部的網頁瀏覽器都會從新發出最後的請求。 若是帶有表單提交的POST請求返回一個常規的響應,那麼刷新將從新提交表單。 由於這不是預期的行爲,因此瀏覽器會要求用戶確認重複的提交,可是大多數用戶卻很難理解瀏覽器詢問的內容。不過,若是一個POST
請求被重定向響應,瀏覽器如今被指示發送GET
請求來獲取重定向中指定的頁面,因此如今最後一個請求再也不是’POST’請求了, 刷新命令就能以更可預測的方式工做。模塊化
這個簡單的技巧叫作Post/Redirect/Get模式。 它避免了用戶在提交網頁表單後無心中刷新頁面時插入重複的動態。
若是你還記得,我建立過幾條模擬的用戶動態,展現在主頁已經有一段時間了。 這些模擬對象是在index
視圖函數中顯式建立的一個簡單的Python列表:
1 posts = [ 2 { 3 'author': {'username': 'John'}, 4 'body': 'Beautiful day in Portland!' 5 }, 6 { 7 'author': {'username': 'Susan'}, 8 'body': 'The Avengers movie was so cool!' 9 } 10 ]
可是如今我在User
模型中有了followed_posts()
方法,它能夠返回給定用戶但願看到的用戶動態的查詢結果集。 因此如今我能夠用真正的用戶動態替換模擬的用戶動態:
app / routes.py:在首頁中顯示真實帖子。
1 @app.route('/', methods=['GET', 'POST']) 2 @app.route('/index', methods=['GET', 'POST']) 3 @login_required 4 def index(): 5 # ... 6 posts = current_user.followed_posts().all() 7 return render_template("index.html", title='Home Page', form=form, 8 posts=posts)
User
類的followed_posts
方法返回一個SQLAlchemy查詢對象,該對象被配置爲從數據庫中獲取用戶感興趣的用戶動態。 在這個查詢中調用all()
會觸發它的執行,返回值是包含全部結果的列表。 因此我最終獲得了一個與我迄今爲止一直使用的模擬用戶動態很是類似的結構。 它們很是接近,模板甚至不須要改變。
相信你已經留意到了,應用沒有一個很好的途徑來讓用戶能夠找到其餘用戶進行關注。實際上,如今根本沒有辦法在頁面上查看到底有哪些用戶存在。我將會使用少許簡單的變動來解決這個問題。
我將會建立一個新的「發現」頁面。該頁面看起來像是主頁,可是卻不是隻顯示已關注用戶的動態,而是展現全部用戶的所有動態。新增的發現視圖函數以下:
app / routes.py:瀏覽視圖功能。
1 @app.route('/explore') 2 @login_required 3 def explore(): 4 posts = Post.query.order_by(Post.timestamp.desc()).all() 5 return render_template('index.html', title='Explore', posts=posts)
你有沒有注意到這個視圖函數中的奇怪之處? render_template()
引用了我在應用的主頁面中使用的index.html模板。 這個頁面與主頁很是類似,因此我決定重用這個模板。 但與主頁不一樣的是,在發現頁面不須要一個發表用戶動態表單,因此在這個視圖函數中,我沒有在模板調用中包含form
參數。
要防止index.html模板在嘗試呈現不存在的Web表單時崩潰,我將添加一個條件,只在傳入表單參數後纔會呈現該表單:
app / templates / index.html:使博客文章提交表單爲可選。
1 {% extends "base.html" %} 2 3 {% block content %} 4 <h1>Hi, {{ current_user.username }}!</h1> 5 {% if form %} 6 <form action="" method="post"> 7 ... 8 </form> 9 {% endif %} 10 ... 11 {% endblock %}
該頁面也須要添加到導航欄中:
app / templates / base.html:連接到導航欄中的瀏覽頁面。
1 <a href="{{ url_for('explore') }}">Explore</a>
還記得我在第六章中介紹的用於我的主頁渲染用戶動態的_post.html子模板嗎? 這是一個包含在我的主頁模板中的小模板,它獨立於其餘模板,所以也能夠被這些模板調用。 我如今要作一個小小的改進,將用戶動態做者的用戶名顯示爲一個連接:
app / templates / _post.html:在博客文章中顯示做者連接。
1 <table> 2 <tr valign="top"> 3 <td><img src="{{ post.author.avatar(36) }}"></td> 4 <td> 5 <a href="{{ url_for('user', username=post.author.username) }}"> 6 {{ post.author.username }} 7 </a> 8 says:<br>{{ post.body }} 9 </td> 10 </tr> 11 </table>
而後在主頁和發現頁中使用這個子模板來渲染用戶動態:
app / templates / index.html:使用博客文章子模板
1 ... 2 {% for post in posts %} 3 {% include '_post.html' %} 4 {% endfor %} 5 ...
子模板指望存在一個名爲post
的變量,才能正常工做。該變量是上層模板中經過循環產生的。
經過這些細小的變動,應用的用戶體驗獲得了大大的提高。如今,用戶能夠訪問發現頁來查看陌生用戶的動態,並經過這些用戶動態來關注用戶,而須要的操做僅僅是點擊用戶名跳轉到其我的主頁並點擊關注連接。使人歎爲觀止!對吧?
此時,我建議你在應用上再次嘗試一下這個功能,以便體驗最後的用戶接口的完善。
應用看起來更完善了,可是在主頁顯示全部用戶動態早晚會出問題。若是一個用戶有成千上萬條關注的用戶動態時,會發生什麼?你能夠想象獲得,管理這麼大的用戶動態列表將會變得至關緩慢和低效。
爲了解決這個問題,我會將用戶動態進行分頁。這意味着一開始顯示的只是全部用戶動態的一部分,並提供連接來訪問其他的用戶動態。Flask-SQLAlchemy的paginate()
方法原生就支持分頁。例如,我想要獲取用戶關注的前20個動態,我能夠將all()
結束調用替換成以下的查詢:
1 >>> user.followed_posts().paginate(1, 20, False).items
Flask-SQLAlchemy的全部查詢對象都支持paginate
方法,須要輸入三個參數來調用它:
True
,當請求範圍超出已知範圍時自動引起404錯誤。若是是False
,則會返回一個空列表。paginate
方法返回一個Pagination
的實例。其items
屬性是請求內容的數據列表。Pagination
實例還有一些其餘用途,我會在以後討論。
如今想一想如何在index()
視圖函數展示分頁呢。我先來給應用添加一個配置項,以表示每頁展現的數據列表長度吧。
config.py:每頁配置數。
1 class Config(object): 2 # ... 3 POSTS_PER_PAGE = 3
存儲這些應用範圍的「可控機關」到配置文件是一個好主意,由於這樣我調整時只需去一個地方。 在最終的應用中,每頁顯示的數據將會大於三,可是對於測試而言,使用小數字很方便。
接下來,我須要決定如何將頁碼併入到應用URL中。 一個至關常見的方法是使用查詢字符串參數來指定一個可選的頁碼,若是沒有給出則默認爲頁面1。 如下是一些示例網址,顯示了我將如何實現這一點:
要訪問查詢字符串中給出的參數,我可使用Flask的request.args對象。 你已經在第五章中看到了這種方法,我用Flask-Login實現了用戶登陸的能夠包含一個next
查詢字符串參數的URL。
給主頁和發現頁的視圖函數添加分頁的代碼變動以下:
app / routes.py:追隨者關聯表
1 @app.route('/', methods=['GET', 'POST']) 2 @app.route('/index', methods=['GET', 'POST']) 3 @login_required 4 def index(): 5 # ... 6 page = request.args.get('page', 1, type=int) 7 posts = current_user.followed_posts().paginate( 8 page, app.config['POSTS_PER_PAGE'], False) 9 return render_template('index.html', title='Home', form=form, 10 posts=posts.items) 11 12 @app.route('/explore') 13 @login_required 14 def explore(): 15 page = request.args.get('page', 1, type=int) 16 posts = Post.query.order_by(Post.timestamp.desc()).paginate( 17 page, app.config['POSTS_PER_PAGE'], False) 18 return render_template("index.html", title='Explore', posts=posts.items)
經過這些更改,這兩個路由決定了要顯示的頁碼,能夠從page
查詢字符串參數得到或是默認值1。而後使用paginate()
方法來檢索指定範圍的結果。 決定頁面數據列表大小的POSTS_PER_PAGE
配置項是經過app.config
對象中獲取的。
請注意,這些更改很是簡單,每次更改都只會影響不多的代碼。 我試圖在編寫應用每一個部分的時候,不作任何有關其餘部分如何工做的假設,這使我能夠編寫更易於擴展和測試的且兼具模塊化和健壯性的應用,而且不太可能失敗或出現BUG。
來嘗試下分頁功能吧。 首先確保你有三條以上的用戶動態。 在發現頁面中更方便測試,由於該頁面顯示全部用戶的動態。 你如今只會看到最近的三條用戶動態。 若是你想看接下來的三條,請在瀏覽器的地址欄中輸入http://localhost:5000/explore?page=2。
接下來的改變是在用戶動態列表的底部添加連接,容許用戶導航到下一頁或上一頁。 還記得我曾提到過paginate()
的返回是Pagination
類的實例嗎? 到目前爲止,我已經使用了此對象的items
屬性,其中包含爲所選頁面檢索的用戶動態列表。 可是這個分頁對象還有一些其餘的屬性在構建分頁連接時頗有用:
has_next
: 當前頁以後存在後續頁面時爲真has_prev
: 當前頁以前存在前置頁面時爲真next_num
: 下一頁的頁碼prev_num
: 上一頁的頁碼有了這四個元素,我就能夠生成上一頁和下一頁的連接並將其傳入模板以渲染:
app / routes.py:下一頁和上一頁連接。
1 @app.route('/', methods=['GET', 'POST']) 2 @app.route('/index', methods=['GET', 'POST']) 3 @login_required 4 def index(): 5 # ... 6 page = request.args.get('page', 1, type=int) 7 posts = current_user.followed_posts().paginate( 8 page, app.config['POSTS_PER_PAGE'], False) 9 next_url = url_for('index', page=posts.next_num) \ 10 if posts.has_next else None 11 prev_url = url_for('index', page=posts.prev_num) \ 12 if posts.has_prev else None 13 return render_template('index.html', title='Home', form=form, 14 posts=posts.items, next_url=next_url, 15 prev_url=prev_url) 16 17 @app.route('/explore') 18 @login_required 19 def explore(): 20 page = request.args.get('page', 1, type=int) 21 posts = Post.query.order_by(Post.timestamp.desc()).paginate( 22 page, app.config['POSTS_PER_PAGE'], False) 23 next_url = url_for('explore', page=posts.next_num) \ 24 if posts.has_next else None 25 prev_url = url_for('explore', page=posts.prev_num) \ 26 if posts.has_prev else None 27 return render_template("index.html", title='Explore', posts=posts.items, 28 next_url=next_url, prev_url=prev_url)
這兩個視圖函數中的next_url
和prev_url
只有在該方向上存在一個頁面時,纔會被設置爲由url_for()
返回的URL。 若是當前頁面位於用戶動態集合的末尾或者開頭,那麼Pagination
實例的has_next
或has_prev
屬性將爲’False’,在這種狀況下,將設置該方向的連接爲None
。
url_for()
函數的一個有趣的地方是,你能夠添加任何關鍵字參數,若是這些參數的名字沒有直接在URL中匹配使用,那麼Flask將它們設置爲URL的查詢字符串參數。
如今讓咱們把它們渲染在index.html模板上,就在用戶動態列表的正下方:
app / templates / index.html:在模板上呈現分頁連接。
1 ... 2 {% for post in posts %} 3 {% include '_post.html' %} 4 {% endfor %} 5 {% if prev_url %} 6 <a href="{{ prev_url }}">Newer posts</a> 7 {% endif %} 8 {% if next_url %} 9 <a href="{{ next_url }}">Older posts</a> 10 {% endif %} 11 ...
主頁和發現頁都添加了分頁連接。第一個連接標記爲「Newer posts」,並指向前一頁(請記住,我顯示的用戶動態按時間的倒序來排序,因此第一頁是最新的內容)。 第二個連接標記爲「Older posts」,並指向下一頁的帖子。 若是這兩個連接中的任何一個都是None
,則經過條件過濾將其從頁面中省略。
主頁分頁已經完成,可是,我的主頁中也有一個用戶動態列表,其中只顯示我的主頁擁有者的動態。 爲了保持一致,我的主頁也應該實現分頁,以匹配主頁的分頁樣式。
我開始更新我的主頁視圖函數,其中仍然有一個模擬用戶動態的列表。
app / routes.py:用戶我的資料視圖功能中的分頁。
1 @app.route('/user/<username>') 2 @login_required 3 def user(username): 4 user = User.query.filter_by(username=username).first_or_404() 5 page = request.args.get('page', 1, type=int) 6 posts = user.posts.order_by(Post.timestamp.desc()).paginate( 7 page, app.config['POSTS_PER_PAGE'], False) 8 next_url = url_for('user', username=user.username, page=posts.next_num) \ 9 if posts.has_next else None 10 prev_url = url_for('user', username=user.username, page=posts.prev_num) \ 11 if posts.has_prev else None 12 return render_template('user.html', user=user, posts=posts.items, 13 next_url=next_url, prev_url=prev_url)
爲了獲得用戶的動態列表,我利用了User
模型中已經定義好的user.posts
一對多關係。 我執行該查詢並添加一個order_by()
子句,以便我首先獲得最新的用戶動態,而後徹底按照我對主頁和發現頁面中的用戶動態所作的那樣進行分頁。 請注意,由url_for()
函數生成的分頁連接須要額外的username
參數,由於它們指向我的主頁,我的主頁依賴用戶名做爲URL的動態組件。
最後,對user.html模板的更改與我在主頁上所作的更改相同:
app / templates / user.html:用戶我的資料模板中的分頁連接。
1 ... 2 {% for post in posts %} 3 {% include '_post.html' %} 4 {% endfor %} 5 {% if prev_url %} 6 <a href="{{ prev_url }}">Newer posts</a> 7 {% endif %} 8 {% if next_url %} 9 <a href="{{ next_url }}">Older posts</a> 10 {% endif %}
完成對分頁功能的實驗後,能夠將POSTS_PER_PAGE
配置項設置爲更合理的值:
config.py:每頁配置數。
1 class Config(object): 2 # ... 3 POSTS_PER_PAGE = 25