配套視頻教程
這章將學習如何對數據庫中條目列表進行分頁。
在上一節,進行了一些必要的數據庫更改,以支持社交網絡大受歡迎的“關注”功能。有了這個功能,我們准備刪除最后一塊腳手架,那就是一開始放置的“假”帖子。在這一章,應用程序將開始接受用戶的博客帖子,並在 /index
頁面和個人資料頁面展示它們。
提交博客帖子
主頁需要一個 表單,用戶在此可以寫新帖子。首先,建立一個表單類: app/forms.py:博客提交表單
#...
class PostForm(FlaskForm):
post = TextAreaField('Say something', validators=[DataRequired(), Length(min=1, max=140)])
submit = SubmitField('Submit')
接着,將上述表單添加到應用程序主頁的模板中:
app/templates/index.html:index模板中帖子提交表單
{% extends "base.html" %}
{% block content %}
<h1>Hello,{{ 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 %}
這個模板的更改與以前的表單處理方式類似。
最后,在視圖函數 index()
中添加上述表單的創建和處理:修改代碼
app/routes.py:在視圖函數中的帖子提交表單
#...
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表單相關的重要事項。注意,在我處理表單數據后,通過發出一個重定向到主頁來結束請求。我可以輕松地跳過重定向,並允許函數繼續向下進入模板渲染部分,因為這已經是index()
視圖函數的功能了。
所以,為什么要重定向?標准做法是通過重定向來響應一個由Web表單提交生成的POST
請求。這有助於緩解一個在Web瀏覽器如何實現刷新命令的煩惱。當點擊 刷新鍵時,所有Web瀏覽器都會重新發出最后一個請求。如果帶有表單提交的POST
請求返回常規響應,那么刷新將重新提交表單。因為這是意料之外的,瀏覽器將要求用戶確認重復提交,但大多數用戶將無法理解瀏覽器詢問的內容。但是,如果POST
請求用重定向來回答請求,則瀏覽器現在指示發送一個GET
請求以獲取重定向中指示的頁面,因此,現在最后一個請求不再是POST請求,刷新命令可以更可預測的方式工作。
這個簡單的技巧稱為:Post/Redirect/Get模式。當用戶在提交Web表單后,無意中刷新頁面時,它可以避免插入重復的帖子。
顯示博客帖子
應該記得,之前創建了一些虛假博客帖子,在主頁上顯示了很長時間了。這些虛擬對象在index()
視圖函數中顯示創建為一個簡單的Python列表:
posts = [
{
'author': {'username': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'username': 'Susan'},
'body': 'The Avengers movie was so cool!'
}
]
但是,現在User模型
中我有followed_posts()
方法,它返回給定用戶想看的帖子的查詢。所以,現在可用真正的帖子替換“假”帖子:
app/routes.py:在主頁顯示真實的帖子
@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查詢對象,這個對象配置為 從數據庫中獲取用戶感興趣的帖子。在這個查詢中調用all()方法會觸發其執行,返回值為一個所有結果集的列表。所以最終得到的結構 跟之前使用的“假”帖子非常相似。正因如此,模板就無須更改了。
flask run
命令運行程序,效果:
C:\Users\Administrator>d:
D:\>cd D:\microblog\venv\Scripts
D:\microblog\venv\Scripts>activate
(venv) D:\microblog\venv\Scripts>cd D:\microblog
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
[2018-08-17 09:59:26,504] INFO in __init__: Microblog startup
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [17/Aug/2018 09:59:36] "GET /index HTTP/1.1" 200 -
補充 提交帖子后的效果。(導航 Explore鏈接是后續添加的),圖略
數據庫查看剛才剛才添加的帖子:
(venv) D:\microblog>sqlite3 app.db
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from post;
1|哈哈哈 測試一下 我是susan2018|2018-08-17 02:03:36.912898|1
sqlite> .quit
(venv) D:\microblog>
輕松地找到用戶和關注
如上述上一小節效果所示,應用程序不能方便地讓用戶找到其他用戶、自己的帖子。事實上,目前為止還沒有辦法查看其他用戶在哪里。接下來,將通過簡單的更改解決這個問題。
創建一個新頁面,稱之為 “Explore”頁面。這個頁面像主頁一樣工作,但不會僅顯示來自所關注用戶的帖子,而是顯示來自所有用戶的全局帖子流。下方是新的 explore()
視圖函數:
app/routes.py:
#...
def index():
#...
@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
模板,它是應用程序的主頁使用的模板。由於這個頁面 跟主頁面非常相似,因此重用index.html
模塊。不過,與主頁面有一個區別是 在/explore
頁面中不需要一個寫博客帖子的表單,所以在這個視圖函數中,沒有在模板中調用 包含form
的參數。
為了防止index.html
模板在呈現不存在的Web表單時崩潰,將添加一個條件,只有在定義時才呈現表單(即傳入表單參數后才會呈現):
app/templates/index.html:讓博客帖子提交表單 可選
{% extends "base.html" %}
{% block content %}
<h1>Hi, {{ current_user.username }}!</h1>
{% if form %}
<form action="" method="post">
...
</form>
{% endif %}
...
{% endblock %}
還需在導航欄中添加指向這個新頁面的鏈接:
app/templates/base.html:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('explore') }}">Explore</a>
_post.html
子模板,用於在用戶個人資料頁面中呈現博客帖子。這是一個包含在用戶個人資料頁面模板中的小模板,是獨立的,因此也可以從其他模板中引用。現在對它進行一些改進,即 將博客帖子作者的用戶名顯示為鏈接:
app/templates/_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>
現在,就可以使用子模板在主頁
、/explore
頁面中呈現博客帖子:
app/templates/index.html:使用博客帖子 子模板
...
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
...
子模板需要一個存在的名為 post
的變量,才能完美地工作,這是在 index模板中 循環變量的命名方式。
通過上述這些微小的變化,應用程序的可用性得到顯著改善。現在,用戶可訪問 /explore
頁 閱讀來自未知用戶的博客帖子,並根據這些帖子找到要關注的新用戶,只需單擊用戶名就可訪問 個人資料頁面。
flask run
運行程序,登錄susan2018、belen、john發幾個帖子,效果:
博客帖子分頁
現在應用程序看起來比以前更好,但是在主頁顯示所有關注的帖子很快將變成一個問題。如果用戶有1000個關注帖子會怎么樣?如果是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()
視圖函數實現分頁。首先,在應用程序config.py中添加一個配置項,以確定每頁顯示多少個項目數。
microblog/config.py:每頁配置的帖子
#...
class Config:
#...
ADMINS = ['your-email@example.com']
POSTS_PER_PAGE = 3
這些應用程序范圍內的 旋鈕能改變配置文件中的行為,因為我能夠去一個簡單的地方做調整。在最終的應用程序中,當然會使用每頁大於3個項目的數字,但是對於測試,使用小數字就有用了。
接下來,我需要決定如何將頁碼合並到應用程序的URL中。一種常見的方法是使用查詢字符串參數來指定可選的頁碼,如果沒有給出,就默認第1頁。下方是一些示例網址,展示將如何實現這一點:
- 第1頁,隱式:http://localhost:5000/index
- 第1頁,顯式:http://localhost:5000/index?page=1
- 第3頁:http://localhost:5000/index?page=3
要訪問查詢字符串中給出的參數,我可以使用Flask的request.args
對象。在第5章中,實現了Flask-Login中可包含next
查詢字符串參數的用戶登錄URL。
下方將可以看到如何向/index
、/explore
視圖函數中添加分頁:
app/routes.py:關注者關聯表
#...
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 Page', 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
查詢字符串參數,或 默認值1,然后使用paginate()
方法取得所需結果的頁面。通過app.config
對象的POSTS_PER_PAGE
配置項 決定了要訪問頁碼的大小。
注意,這些更改很容易,以及 每次更改代碼的影響程度如何。我正嘗試編寫應用程序的每個部分,而不對其他部分如何工作做任何假設,這使我能夠編寫更易於擴展和測試的模塊化、健壯的應用程序,並且不太可能出現故障 或bug。
運行程序,測試上述所編寫的分頁支持。首先,確保3篇以上的帖子。這在 /explore
頁面很容易看到,這個頁面顯示所有用戶的帖子。目前將只會看到最近的3篇帖子。若要查詢下一個3篇帖子,可在瀏覽器地址欄輸入:http://localhost:5000/explore?page=2
頁面導航
下一個更改是 在博客帖子列表底部添加鏈接,允許用戶導航到下一頁 或上一頁。還記得 調用paginate()
方法的返回值是一個Flask-SQLAlchemy的Pagination類
的一個對象?目前為止,我們已經使用了這個對象的items
屬性,它包含為所選頁面檢索的項目列表。但是 這個對象還具有一些在構建分頁鏈接時有用的前提屬性:
has_next
:如果當前頁面后面至少還有一頁,則為True;has_prev
:如果在當前頁面之前至少還有一頁,則為True;next_num
:下一頁的頁碼;prev_num
:上一頁的頁碼。
通過上述4個屬性,可生成下一個和上一個頁面鏈接,並將它們傳遞給模板進行渲染:
app/routes.py:下一頁和上一頁鏈接
#...
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 Page', 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)
上述兩個視圖函數的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:在模板中渲染分頁鏈接
...
{% 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 %}
...
這個更改會在 /index
頁、/explore
頁上的帖子列表下添加鏈接。第一個鏈接標記為“Newer posts”,指向上一頁(注意,顯示帖子按最新排序,因此第一頁是具有最新內容的頁面)。第二個鏈接標記為“Older posts”,指向帖子的下一頁。如果這兩個鏈接中的任何一個是None
,則通過條件從頁面中省略它。
運行程序,效果:
在用戶個人資料頁面分頁
/index
頁面的更改現在已足夠。但是,用戶個人資料頁面中還有一個帖子列表,其僅顯示來自個人資料所有者的帖子。為了保持一致,應更改用戶個人資料頁面以匹配 /index
頁面的分頁樣式。
首先,更新用戶個人資料的視圖函數,其中仍然有一個“假”帖子的對象列表。
app/routes.py:用戶個人資料頁面視圖中的分頁
#...
@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
關系是一個通過SQLAlchemy</b已經建立的查詢,它是在User
模型中由db.relationship()
定義的結果。我們接受這個查詢,並添加一個order_by()
子句,以便首先取得最新的帖子,然后像對 /index
和 /explore
頁中那樣完成分頁。注意,url_for()
函數生成的分頁鏈接 需要額外的 username
參數,因為它們指向用戶個人資料頁面,這個頁面具有這個用戶名作為URL的動態組件。
最后,對user.html
模板的更改 與在/index
頁面上所做 的更改相同:
app/templates/user.html:用戶個人資料頁面模板的分頁鏈接
#...
{% for post in posts %}
{% include '_post.html' %}
{% endfor%}
{% if prev_url %}
<a href="{{ prev_url }}">Newer posts</a>
{% if next_url %}
<a href="{{ next_url }}">Older posts</a>
{% endif %}
{% endblock%}
完成分頁功能的實驗后,可將POSTS_PER_PAGE
配置項設置為更合理的值:
microblog/config.py:每個頁面配置 帖子
class Config(object):
# ...
POSTS_PER_PAGE = 25
目前為止,項目結構
microblog/
app/
templates/
_post.html
404.html
500.html
base.html
edit_profile.html
index.html
login.html
register.html
user.html
__init__.py
errors.py
forms.py
models.py
routes.py
logs/
microblog.log
migrations/
venv/
app.db
config.py
microblog.py
tests.py
參考
https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-ix-pagination