(九)Flask 學習 —— 分頁

分頁

回顧

在前面的章節(關注者,聯繫人和好友),咱們已經完成了全部支持 「關注者」 功能的數據庫的修改。今天咱們將會讓咱們應用程序接受用戶的真實數據。咱們將要告別僞造數據的時候!html

咱們接下來說述的正是咱們上一章離開的地方,因此你可能要確保應用程序 microblog 正確地安裝和工做。python

提交博客文章

讓咱們先以簡單的內容開始,主頁應該有一個提交新的 blog 的表單。git

首先咱們定義一個單字段的表單對象(文件 app/forms.py):github

class PostForm(Form):
    post = StringField('post', validators=[DataRequired()])

接着,咱們把表單添加到模板中(文件 app/templates/index.html):sql

<!-- extend base layout -->
{% extends "base.html" %}

{% block content %}
<h1>Hi, {{g.user.nickname}}!</h1>
<form action="" method="post" name="post">
    {{form.hidden_tag()}}
    <table>
        <tr>
            <td>Say something:</td>
            <td>{{ form.post(size = 30, maxlength = 140) }}</td>
            <td>
            {% for error in form.errors.post %}
            <span style="color: red;">[{{error}}]</span><br>
            {% endfor %}
            </td>
        </tr>
        <tr>
            <td></td>
            <td><input type="submit" value="Post!"></td>
            <td></td>
        </tr>
    </table>
</form>
{% for post in posts %}
<p>
  {{post.author.nickname}} says: <b>{{post.body}}</b>
</p>
{% endfor %}
{% endblock %}

到目前爲止,內容沒有什麼解釋的,都不是新的東西。咱們只是簡單的添加另一個表單而已,跟咱們以前作的同樣。數據庫

最後,把這一切聯繫起來的視圖函數須要被擴展用來處理表單(文件 app/views.py):flask

from forms import LoginForm, EditForm, PostForm
from models import User, 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, timestamp=datetime.utcnow(), author=g.user)
        db.session.add(post)
        db.session.commit()
        flash('Your post is now live!')
        return redirect(url_for('index'))
    posts = [
        {
            'author': {'nickname': 'John'},
            'body': 'Beautiful day in Portland!'
        },
        {
            'author': {'nickname': 'Susan'},
            'body': 'The Avengers movie was so cool!'
        }
    ]
    return render_template('index.html',
                           title='Home',
                           form=form,
                           posts=posts)

讓咱們一個一個來回顧下這個函數的修改點:瀏覽器

  • 導入 Post 和 PostForm 類session

  • 在與 index 視圖函數相關聯的兩個路由上,咱們接受 POST 請求,由於咱們須要接受提交的 blog。app

  • 當接受常規的 GET 請求的時候咱們像之前同樣的處理。當咱們接收到一個表單的提交的時候,咱們在數據庫中插入一個新的 Post 記錄。

  • 模板如今接受一個新的參數:form

當咱們在數據庫中插入一個新的 Post 後,咱們將會重定向到首頁:

return redirect(url_for('index'))

咱們這裏能夠輕鬆地跳過(不使用)重定向,容許函數繼續到渲染模板的部分,這將會是更加高效的。所以,爲何須要重定向?考慮若是一個用戶正在撰寫 blog,接着不當心按到了瀏覽器的刷新鍵,會發生些什麼。刷新的命令將會作些什麼?瀏覽器將會從新發送上一次的請求做爲刷新命令的結果。

沒有重定向,上一次的請求是提交表單的 POST 請求,所以刷新動做將會從新提交表單,致使與第一個相同的第二個 Post 記錄被寫入數據庫。這並很差。

有了重定向,咱們迫使瀏覽器在表單提交後發送另一個請求,即重定向頁的請求。這是一個簡單的 GET 請求,所以一個刷新動做將會重複 GET 請求而不是屢次提交表單。

這個小技巧避免了用戶在提交 blog 後不當心觸發刷新的動做而致使插入重複的 blog。

顯示 blog

咱們將要從數據庫獲取 blog,並展現它們。

若是你還記得前幾篇文章中,咱們建立了幾個僞造的 blog,它們已經在咱們主頁上展現很長一段時間。在 index 視圖函數這兩個建立的僞造的對象是簡單的 Python 列表:

posts = [
    {
        'author': { 'nickname': 'John' },
        'body': 'Beautiful day in Portland!'
    },
    {
        'author': { 'nickname': 'Susan' },
        'body': 'The Avengers movie was so cool!'
    }]

可是在上一章中咱們已經建立了一個查詢,它容許咱們獲取關注的用戶的全部的 blog,所以咱們簡單地替換上面這些僞造的數據(文件 app/views.py):

posts = g.user.followed_posts().all()

當你運行應用程序的時候就會看到來自數據庫中的 blog。

User 類中的 followed_posts 方法返回一個 sqlalchemy 查詢對象,該查詢對象用於獲取咱們感興趣的 blog。在這個查詢中調用 all() 只是爲了檢索全部的 blog 並造成一個列表,所以咱們以一個與咱們使用的僞造數據類似的結構結束。模版是不會注意到這一點的。

這個時候能夠接着試試你的應用程序了。你能夠建立一些用戶,接着關注他們(她們),最後發佈些 blog。

分頁

應用程序看起來比任什麼時候候都要好,可是仍是有個問題。咱們把全部關注者的 blog 展現在首頁上。若是數量超過上千的話會發生些什麼?或者上百萬?你能夠想象獲得,處理如此大數據量的列表對象將會及其低效的。

相反,若是咱們分組或者分頁顯示大量的 blog?效率和效果會不會好一些了?

Flask-SQLAlchemy 天生就支持分頁。好比若是咱們想要獲得用戶關注者的前三篇 blog,咱們能夠這樣作:

posts = g.user.followed_posts().paginate(1, 3, False).items

paginate 方法可以被任何查詢調用。它接受三個參數:

  • 頁數,從 1 開始,

  • 每一頁的項目數,這裏也就是說每一頁顯示的 blog 數,

  • 錯誤標誌。若是是 True,當請求的範圍頁超出範圍的話,一個 404 錯誤將會自動地返回到客戶端的網頁瀏覽器。若是是 False,返回一個空列表而不是錯誤。

從 paginate 返回的值是一個 Pagination 對象。這個對象的 items 成員包含了請求頁面項目(本文是指 blog)的列表。在 Pagination 對象中還有其它有幫助的東西,咱們將在後面能看到。

如今讓咱們想一想如何在咱們的 index 視圖函數中實現分頁。咱們首先在配置文件中添加一些決定每頁顯示的 blog 數的配置項(文件 config.py):

# paginationPOSTS_PER_PAGE = 3

在最後的應用程序中咱們固然會使用每頁顯示的 blog 數大於 3,可是測試的時候用小的數量更加方便。

接着,讓咱們看看不一樣頁的 URLs 是什麼樣的。咱們知道 Flask 路由能夠攜帶參數,所以咱們在 URL 後添加一個後綴表示所需的頁面:

http://localhost:5000/         <-- page #1 (default)
http://localhost:5000/index    <-- page #1 (default)
http://localhost:5000/index/1  <-- page #1
http://localhost:5000/index/2  <-- page #2

這種格式的 URLs 可以輕易地經過在咱們的視圖函數中附加一個 route 來實現(文件 app/views.py):

from config import POSTS_PER_PAGE

@app.route('/', methods = ['GET', 'POST'])
@app.route('/index', methods = ['GET', 'POST'])
@app.route('/index/<int:page>', methods = ['GET', 'POST'])
@login_required
def index(page = 1):
    form = PostForm()
    if form.validate_on_submit():
        post = Post(body = form.post.data, timestamp = datetime.utcnow(), author = g.user)
        db.session.add(post)
        db.session.commit()
        flash('Your post is now live!')
        return redirect(url_for('index'))
    posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False).items
    return render_template('index.html',
        title = 'Home',
        form = form,
        posts = posts)

咱們新的路由須要頁面數做爲參數,而且聲明爲一個整型。一樣咱們也須要在 index 函數中添加 page 參數,而且咱們須要給它一個默認值。

如今咱們已經有可用的頁面數,咱們可以很容易地把它與配置中的 POSTS_PER_PAGE 一塊兒傳入 followed_posts 查詢。

如今試試輸入不一樣的 URLs,看看分頁的效果。可是,須要確保可用的 blog 數要超過三個,這樣你就可以看到不止一頁了!

頁面導航

咱們如今須要添加連接容許用戶訪問下一頁以及/或者前一頁,幸虧這是很容易作的,Flask-SQLAlchemy 爲咱們作了大部分工做。

咱們如今開始在視圖函數中作一些小改變。在咱們目前的版本中咱們按以下方式使用 paginate 方法:

posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False).items

經過上面這樣作,咱們能夠得到返回自 paginate 的 Pagination 對象的 items 成員。可是這個對象還有不少其它有用的東西在裏面,所以咱們仍是使用整個對象(文件 app/views.py):

posts = g.user.followed_posts().paginate(page, POSTS_PER_PAGE, False)

爲了適應這種改變,咱們必須修改模板(文件 app/templates/index.html):

<!-- posts is a Paginate object -->
{% for post in posts.items %}
<p>
  {{post.author.nickname}} says: <b>{{post.body}}</b>
</p>
{% endfor %}

這個改變使得模版可以使用徹底的 Paginate 對象。咱們使用的這個對象的成員有:

  • has_next:若是在目前頁後至少還有一頁的話,返回 True

  • has_prev:若是在目前頁以前至少還有一頁的話,返回 True

  • next_num:下一頁的頁面數

  • prev_num:前一頁的頁面數

有了這些元素後,咱們產生了這些(文件 app/templates/index.html):

<!-- posts is a Paginate object -->
{% for post in posts.items %}
<p>
  {{post.author.nickname}} says: <b>{{post.body}}</b>
</p>
{% endfor %}
{% if posts.has_prev %}<a href="{{ url_for('index', page = posts.prev_num) }}"><< Newer posts</a>{% else %}<< Newer posts{% endif %} |
{% if posts.has_next %}<a href="{{ url_for('index', page = posts.next_num) }}">Older posts >></a>{% else %}Older posts >>{% endif %}

所以,咱們有了兩個連接。第一個就是名爲 「Newer posts」,這個連接使得咱們可以訪問上一頁。第二個就是 「Older posts」,它指向下一頁。

當咱們瀏覽第一頁的時候,咱們不但願看到有上一頁的連接,由於這時候是不存在前一頁。這是很容易被監測的,由於 posts.has_prev 會是 False。咱們簡單地處理這種狀況,當用戶瀏覽首頁的時候,上一頁會顯示出來,可是不會有任何的連接。一樣,下一頁也是這樣的處理方式。

實現 Post 子模板

在前面的章節中,咱們定義了一個子模板來渲染單個 blog。定義這個子模板的緣由是在多個使用單個 blog 的頁面的時候只須要包含這個子模板就好了,不須要重複拷貝 HTML 代碼。

如今是時候在咱們的首頁上也包含這個子模板。大部分的事情都已經作完了,所以這是很簡單的(文件 app/templates/index.html):

<!-- posts is a Paginate object -->
{% for post in posts.items %}
    {% include 'post.html' %}
{% endfor %}

驚人吧?咱們只是丟棄舊的渲染代碼,取而代之的是一個包含子模板新的 HTML 代碼。就只作了這一點,咱們獲得更好的版本。

下面是目前應用程序的截圖:

_images/9.png

用戶信息頁

首頁上的分頁已經完成了。然而,咱們在用戶信息頁上顯示了 blog。爲了保持一致性,用戶信息頁也跟首頁同樣。

改變是跟修改首頁同樣的。這是咱們須要作的列表:

  • 添加一個額外的路由獲取頁面數的參數

  • 添加一個默認值爲 1 的 page 參數到視圖函數

  • 用合適的數據庫查詢與分頁代替僞造的 blog

  • 更新模板使用分頁對象

下面就是更新後的視圖函數(文件 app/views.py):

@app.route('/user/<nickname>')@app.route('/user/<nickname>/<int:page>')@login_requireddef user(nickname, page=1):
    user = User.query.filter_by(nickname=nickname).first()
    if user is None:
        flash('User %s not found.' % nickname)
        return redirect(url_for('index'))
    posts = user.posts.paginate(page, POSTS_PER_PAGE, False)
    return render_template('user.html',
                           user=user,
                           posts=posts)

注意上面的視圖函數已經有一個 nickname 參數,咱們把 page 做爲它的第二個參數。

模版的改變一樣很簡單(文件 app/templates/user.html):

<!-- posts is a Paginate object -->
{% for post in posts.items %}
    {% include 'post.html' %}
{% endfor %}
{% if posts.has_prev %}<a href="{{ url_for('user', nickname = user.nickname, page = posts.prev_num) }}"><< Newer posts</a>{% else %}<< Newer posts{% endif %} |
{% if posts.has_next %}<a href="{{ url_for('user', nickname = user.nickname, page = posts.next_num) }}">Older posts >></a>{% else %}Older posts >>{% endif %}

結束語

代碼中更新了本文中的一些修改,若是你想要節省時間的話,你能夠下載 microblog-0.9.zip

相關文章
相關標籤/搜索