14.flask博客項目實戰九之分頁功能


配套視頻教程

本文B站配套視頻教程

這章將學習如何對數據庫中條目列表進行分頁。

在上一節,進行了一些必要的數據庫更改,以支持社交網絡大受歡迎的“關注”功能。有了這個功能,我們准備刪除最后一塊腳手架,那就是一開始放置的“假”帖子。在這一章,應用程序將開始接受用戶的博客帖子,並在 /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)
#...

逐一查看視圖函數中的更改:

  1. 導入 PostPostForm類;
  2. GET請求外,關聯到兩個路由的視圖函數index()都接受POST請求,因為這個視圖函數現在將接收表單數據;
  3. 表單處理邏輯將一個新Post記錄 插入數據庫;
  4. 模板接收 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發幾個帖子,效果:
image.png

博客帖子分頁

現在應用程序看起來比以前更好,但是在主頁顯示所有關注的帖子很快將變成一個問題。如果用戶有1000個關注帖子會怎么樣?如果是100萬呢?可以想象,管理如此龐大的帖子列表將很緩慢且效率低下。

為解決這個問題,我將對帖子列表進行分頁。這意味着,最初將一次只顯示有限數量的帖子,並包含用於瀏覽整個帖子列表的鏈接。Flask-SQLAlchemy本身支持使用paginate()查詢方法進行分頁。例如,如果想獲得用戶的前20個帖子,可以用如下代碼替換all()終止查詢:

>>>user.followed_posts().paginate(1,20,False).items

paginate()方法可以在Flask_SQLAlchemy的任何查詢對象上調用。它有3個參數:

  1. 頁碼,從1開始;
  2. 每頁的項目數;
  3. 錯誤標志。若為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頁。下方是一些示例網址,展示將如何實現這一點:

下方將可以看到如何向/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-SQLAlchemyPagination類的一個對象?目前為止,我們已經使用了這個對象的items屬性,它包含為所選頁面檢索的項目列表。但是 這個對象還具有一些在構建分頁鏈接時有用的前提屬性:

  1. has_next:如果當前頁面后面至少還有一頁,則為True;
  2. has_prev:如果在當前頁面之前至少還有一頁,則為True;
  3. next_num:下一頁的頁碼;
  4. 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_nexthas_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,則通過條件從頁面中省略它。

運行程序,效果:
image.png

在用戶個人資料頁面分頁

/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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM