在前面的章节(关注者,联系人和好友),咱们已经完成了全部支持 “关注者” 功能的数据库的修改。今天咱们将会让咱们应用程序接受用户的真实数据。咱们将要告别伪造数据的时候!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。