【百度云搜索省核,搜各種資料:http://bdy.lqkweb.com】
【搜網(wǎng)盤迁杨,搜各種資料:http://www.swpan.cn】
本文翻譯自The Flask Mega-Tutorial Part IX: Pagination
這是Flask Mega-Tutorial系列的第九部分,我將告訴你如何對數(shù)據(jù)列表進行分頁玩焰。
在第八章我已經(jīng)做了幾個數(shù)據(jù)庫更改,以支持在社交網(wǎng)絡(luò)非常流行的“粉絲”機制。 有了這個功能拯田,接下來我準備好刪除一開始就使用的模擬用戶動態(tài)了。 在本章中甩十,應(yīng)用將開始接受來自用戶的動態(tài)更新船庇,并將其發(fā)布到網(wǎng)站首頁和個人主頁。
本章的GitHub鏈接為:Browse, Zip, Diff.
發(fā)布用戶動態(tài)
讓我們從簡單的事情開始吧侣监。 首頁需要有一個表單鸭轮,用戶可以在其中鍵入新動態(tài)。 我創(chuàng)建一個表單類:
class PostForm(FlaskForm):
post = TextAreaField('Say something', validators=[
DataRequired(), Length(min=1, max=140)])
submit = SubmitField('Submit')
然后橄霉,我將該表單添加到網(wǎng)站首頁的模板中:
{% 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 %}
模板中的變更和處理以前的表單類似窃爷。最后的部分是將表單處理邏輯添加到視圖函數(shù)中:
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)
我們來一個個地解讀該視圖函數(shù)的變更:
- 導(dǎo)入
Post
和PostForm
類 - 關(guān)聯(lián)到
index
視圖函數(shù)的兩個路由都新增接受POST
請求,以便視圖函數(shù)處理接收的表單數(shù)據(jù) - 處理表單的邏輯會為
post
表插入一條新的數(shù)據(jù) - 模板新增接受
form
對象姓蜂,以便渲染文本輸入框
在繼續(xù)之前按厘,我想提一些與Web表單處理相關(guān)的重要內(nèi)容。 請注意钱慢,在處理表單數(shù)據(jù)后逮京,我通過發(fā)送重定向到主頁來結(jié)束請求。 我可以輕松地跳過重定向束莫,并允許函數(shù)繼續(xù)向下進入模板渲染部分懒棉,因為這已經(jīng)是主頁視圖函數(shù)了。
那么览绿,為什么重定向呢策严? 通過重定向來響應(yīng)Web表單提交產(chǎn)生的POST請求是一種標準做法。 這有助于緩解在Web瀏覽器中執(zhí)行刷新命令的煩惱饿敲。 當你點擊刷新鍵時妻导,所有的網(wǎng)頁瀏覽器都會重新發(fā)出最后的請求。 如果帶有表單提交的POST請求返回一個常規(guī)的響應(yīng),那么刷新將重新提交表單倔韭。 因為這不是預(yù)期的行為暑脆,所以瀏覽器會要求用戶確認重復(fù)的提交,但是大多數(shù)用戶卻很難理解瀏覽器詢問的內(nèi)容狐肢。不過添吗,如果一個POST
請求被重定向響應(yīng),瀏覽器現(xiàn)在被指示發(fā)送GET
請求來獲取重定向中指定的頁面份名,所以現(xiàn)在最后一個請求不再是’POST’請求了碟联, 刷新命令就能以更可預(yù)測的方式工作。
這個簡單的技巧叫做Post/Redirect/Get模式僵腺。 它避免了用戶在提交網(wǎng)頁表單后無意中刷新頁面時插入重復(fù)的動態(tài)鲤孵。
展示用戶動態(tài)
如果你還記得,我創(chuàng)建過幾條模擬的用戶動態(tài)辰如,展示在主頁已經(jīng)有一段時間了普监。 這些模擬對象是在index
視圖函數(shù)中顯式創(chuàng)建的一個簡單的Python列表:
posts = [
{
'author': {'username': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'username': 'Susan'},
'body': 'The Avengers movie was so cool!'
}
]
但是現(xiàn)在我在User
模型中有了followed_posts()
方法,它可以返回給定用戶希望看到的用戶動態(tài)的查詢結(jié)果集琉兜。 所以現(xiàn)在我可以用真正的用戶動態(tài)替換模擬的用戶動態(tài):
@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
方法返回一個SQLAlchemy查詢對象凯正,該對象被配置為從數(shù)據(jù)庫中獲取用戶感興趣的用戶動態(tài)。 在這個查詢中調(diào)用all()
會觸發(fā)它的執(zhí)行豌蟋,返回值是包含所有結(jié)果的列表廊散。 所以我最終得到了一個與我迄今為止一直使用的模擬用戶動態(tài)非常相似的結(jié)構(gòu)。 它們非常接近梧疲,模板甚至不需要改變允睹。
更容易地發(fā)現(xiàn)和關(guān)注用戶
相信你已經(jīng)留意到了,應(yīng)用沒有一個很好的途徑來讓用戶可以找到其他用戶進行關(guān)注幌氮。實際上缭受,現(xiàn)在根本沒有辦法在頁面上查看到底有哪些用戶存在。我將會使用少量簡單的變更來解決這個問題该互。
我將會創(chuàng)建一個新的“發(fā)現(xiàn)”頁面米者。該頁面看起來像是主頁,但是卻不是只顯示已關(guān)注用戶的動態(tài)慢洋,而是展示所有用戶的全部動態(tài)塘雳。新增的發(fā)現(xiàn)視圖函數(shù)如下:
@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)
你有沒有注意到這個視圖函數(shù)中的奇怪之處陆盘? render_template()
引用了我在應(yīng)用的主頁面中使用的index.html模板普筹。 這個頁面與主頁非常相似,所以我決定重用這個模板隘马。 但與主頁不同的是太防,在發(fā)現(xiàn)頁面不需要一個發(fā)表用戶動態(tài)表單,所以在這個視圖函數(shù)中,我沒有在模板調(diào)用中包含form
參數(shù)蜒车。
要防止index.html模板在嘗試呈現(xiàn)不存在的Web表單時崩潰讳嘱,我將添加一個條件,只在傳入表單參數(shù)后才會呈現(xiàn)該表單:
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% if form %}
<form action="" method="post">
...
</form>
{% endif %}
...
{% endblock %}
該頁面也需要添加到導(dǎo)航欄中:
<a href="{{ url_for('explore') }}">Explore</a>
還記得我在第六章中介紹的用于個人主頁渲染用戶動態(tài)的_post.html子模板嗎酿愧? 這是一個包含在個人主頁模板中的小模板沥潭,它獨立于其他模板,因此也可以被這些模板調(diào)用嬉挡。 我現(xiàn)在要做一個小小的改進钝鸽,將用戶動態(tài)作者的用戶名顯示為一個鏈接:
<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>
然后在主頁和發(fā)現(xiàn)頁中使用這個子模板來渲染用戶動態(tài):
...
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
...
子模板期望存在一個名為post
的變量,才能正常工作庞钢。該變量是上層模板中通過循環(huán)產(chǎn)生的拔恰。
通過這些細小的變更,應(yīng)用的用戶體驗得到了大大的提升』ǎ現(xiàn)在颜懊,用戶可以訪問發(fā)現(xiàn)頁來查看陌生用戶的動態(tài),并通過這些用戶動態(tài)來關(guān)注用戶风皿,而需要的操作僅僅是點擊用戶名跳轉(zhuǎn)到其個人主頁并點擊關(guān)注鏈接河爹。令人嘆為觀止!對吧桐款?
此時昌抠,我建議你在應(yīng)用上再次嘗試一下這個功能,以便體驗最后的用戶接口的完善鲁僚。
用戶動態(tài)的分頁
應(yīng)用看起來更完善了炊苫,但是在主頁顯示所有用戶動態(tài)遲早會出問題。如果一個用戶有成千上萬條關(guān)注的用戶動態(tài)時冰沙,會發(fā)生什么侨艾?你可以想象得到,管理這么大的用戶動態(tài)列表將會變得相當緩慢和低效拓挥。
為了解決這個問題唠梨,我會將用戶動態(tài)進行分頁。這意味著一開始顯示的只是所有用戶動態(tài)的一部分侥啤,并提供鏈接來訪問其余的用戶動態(tài)当叭。Flask-SQLAlchemy的paginate()
方法原生就支持分頁。例如盖灸,我想要獲取用戶關(guān)注的前20個動態(tài)蚁鳖,我可以將all()
結(jié)束調(diào)用替換成如下的查詢:
>>> user.followed_posts().paginate(1, 20, False).items
Flask-SQLAlchemy的所有查詢對象都支持paginate
方法,需要輸入三個參數(shù)來調(diào)用它:
- 從1開始的頁碼
- 每頁的數(shù)據(jù)量
- 錯誤處理布爾標記赁炎,如果是
True
醉箕,當請求范圍超出已知范圍時自動引發(fā)404錯誤。如果是False
,則會返回一個空列表讥裤。
paginate
方法返回一個Pagination
的實例放棒。其items
屬性是請求內(nèi)容的數(shù)據(jù)列表。Pagination
實例還有一些其他用途己英,我會在之后討論间螟。
現(xiàn)在想想如何在index()
視圖函數(shù)展現(xiàn)分頁呢。我先來給應(yīng)用添加一個配置項损肛,以表示每頁展示的數(shù)據(jù)列表長度吧寒亥。
class Config(object):
# ...
POSTS_PER_PAGE = 3
存儲這些應(yīng)用范圍的“可控機關(guān)”到配置文件是一個好主意,因為這樣我調(diào)整時只需去一個地方荧关。 在最終的應(yīng)用中溉奕,每頁顯示的數(shù)據(jù)將會大于三,但是對于測試而言忍啤,使用小數(shù)字很方便加勤。
接下來,我需要決定如何將頁碼并入到應(yīng)用URL中同波。 一個相當常見的方法是使用查詢字符串參數(shù)來指定一個可選的頁碼鳄梅,如果沒有給出則默認為頁面1。 以下是一些示例網(wǎng)址未檩,顯示了我將如何實現(xiàn)這一點:
- 第1頁戴尸,隱含:http://localhost:5000/index
- 第1頁,顯式:http://localhost:5000/index?page=1
- 第3頁:http://localhost:5000/index?page=3
要訪問查詢字符串中給出的參數(shù)冤狡,我可以使用Flask的request.args對象孙蒙。 你已經(jīng)在第五章中看到了這種方法,我用Flask-Login實現(xiàn)了用戶登錄的可以包含一個next
查詢字符串參數(shù)的URL悲雳。
給主頁和發(fā)現(xiàn)頁的視圖函數(shù)添加分頁的代碼變更如下:
@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)
通過這些更改挎峦,這兩個路由決定了要顯示的頁碼,可以從page
查詢字符串參數(shù)獲得或是默認值1合瓢。然后使用paginate()
方法來檢索指定范圍的結(jié)果坦胶。 決定頁面數(shù)據(jù)列表大小的POSTS_PER_PAGE
配置項是通過app.config
對象中獲取的。
請注意晴楔,這些更改非常簡單顿苇,每次更改都只會影響很少的代碼。 我試圖在編寫應(yīng)用每個部分的時候税弃,不做任何有關(guān)其他部分如何工作的假設(shè)纪岁,這使我可以編寫更易于擴展和測試的且兼具模塊化和健壯性的應(yīng)用,并且不太可能失敗或出現(xiàn)BUG钙皮。
來嘗試下分頁功能吧蜂科。 首先確保你有三條以上的用戶動態(tài)顽决。 在發(fā)現(xiàn)頁面中更方便測試短条,因為該頁面顯示所有用戶的動態(tài)导匣。 你現(xiàn)在只會看到最近的三條用戶動態(tài)。 如果你想看接下來的三條茸时,請在瀏覽器的地址欄中輸入http://localhost:5000/explore?page=2贡定。
分頁導(dǎo)航
接下來的改變是在用戶動態(tài)列表的底部添加鏈接,允許用戶導(dǎo)航到下一頁或上一頁可都。 還記得我曾提到過paginate()
的返回是Pagination
類的實例嗎缓待? 到目前為止,我已經(jīng)使用了此對象的items
屬性渠牲,其中包含為所選頁面檢索的用戶動態(tài)列表旋炒。 但是這個分頁對象還有一些其他的屬性在構(gòu)建分頁鏈接時很有用:
-
has_next
: 當前頁之后存在后續(xù)頁面時為真 -
has_prev
: 當前頁之前存在前置頁面時為真 -
next_num
: 下一頁的頁碼 -
prev_num
: 上一頁的頁碼
有了這四個元素,我就可以生成上一頁和下一頁的鏈接并將其傳入模板以渲染:
@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)
這兩個視圖函數(shù)中的next_url
和prev_url
只有在該方向上存在一個頁面時签杈,才會被設(shè)置為由url_for()
返回的URL瘫镇。 如果當前頁面位于用戶動態(tài)集合的末尾或者開頭,那么Pagination
實例的has_next
或has_prev
屬性將為’False’答姥,在這種情況下铣除,將設(shè)置該方向的鏈接為None
。
url_for()
函數(shù)的一個有趣的地方是鹦付,你可以添加任何關(guān)鍵字參數(shù)尚粘,如果這些參數(shù)的名字沒有直接在URL中匹配使用,那么Flask將它們設(shè)置為URL的查詢字符串參數(shù)敲长。
現(xiàn)在讓我們把它們渲染在index.html模板上郎嫁,就在用戶動態(tài)列表的正下方:
...
{% 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 %}
...
主頁和發(fā)現(xiàn)頁都添加了分頁鏈接。第一個鏈接標記為“Newer posts”祈噪,并指向前一頁(請記住行剂,我顯示的用戶動態(tài)按時間的倒序來排序,所以第一頁是最新的內(nèi)容)钳降。 第二個鏈接標記為“Older posts”厚宰,并指向下一頁的帖子。 如果這兩個鏈接中的任何一個都是None
遂填,則通過條件過濾將其從頁面中省略铲觉。
個人主頁中的分頁
主頁分頁已經(jīng)完成,但是吓坚,個人主頁中也有一個用戶動態(tài)列表撵幽,其中只顯示個人主頁擁有者的動態(tài)。 為了保持一致礁击,個人主頁也應(yīng)該實現(xiàn)分頁盐杂,以匹配主頁的分頁樣式逗载。
我開始更新個人主頁視圖函數(shù),其中仍然有一個模擬用戶動態(tài)的列表链烈。
@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)
為了得到用戶的動態(tài)列表厉斟,我利用了User
模型中已經(jīng)定義好的user.posts
一對多關(guān)系。 我執(zhí)行該查詢并添加一個order_by()
子句强衡,以便我首先得到最新的用戶動態(tài)擦秽,然后完全按照我對主頁和發(fā)現(xiàn)頁面中的用戶動態(tài)所做的那樣進行分頁。 請注意漩勤,由url_for()
函數(shù)生成的分頁鏈接需要額外的username
參數(shù)感挥,因為它們指向個人主頁,個人主頁依賴用戶名作為URL的動態(tài)組件越败。
最后触幼,對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
配置項設(shè)置為更合理的值:
class Config(object):
# ...
POSTS_PER_PAGE = 25