在前面的章節(關注者,聯繫人和好友),咱們已經完成了全部支持 「關注者」 功能的數據庫的修改。今天咱們將會讓咱們應用程序接受用戶的真實數據。咱們將要告別僞造數據的時候!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,它們已經在咱們主頁上展現很長一段時間。在 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。咱們簡單地處理這種狀況,當用戶瀏覽首頁的時候,上一頁會顯示出來,可是不會有任何的連接。一樣,下一頁也是這樣的處理方式。
在前面的章節中,咱們定義了一個子模板來渲染單個 blog。定義這個子模板的緣由是在多個使用單個 blog 的頁面的時候只須要包含這個子模板就好了,不須要重複拷貝 HTML 代碼。
如今是時候在咱們的首頁上也包含這個子模板。大部分的事情都已經作完了,所以這是很簡單的(文件 app/templates/index.html):
<!-- posts is a Paginate object --> {% for post in posts.items %} {% include 'post.html' %} {% endfor %}
驚人吧?咱們只是丟棄舊的渲染代碼,取而代之的是一個包含子模板新的 HTML 代碼。就只作了這一點,咱們獲得更好的版本。
下面是目前應用程序的截圖:
首頁上的分頁已經完成了。然而,咱們在用戶信息頁上顯示了 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。