Flask 教程 第九章:分页

本文翻译自The Flask Mega-Tutorial Part IX: Paginationhtml

这是Flask Mega-Tutorial系列的第九部分,我将告诉你如何对数据列表进行分页。git

第八章我已经作了几个数据库更改,以支持在社交网络很是流行的“粉丝”机制。 有了这个功能,接下来我准备好删除一开始就使用的模拟用户动态了。 在本章中,应用将开始接受来自用户的动态更新,并将其发布到网站首页和我的主页。github

本章的GitHub连接为:BrowseZipDiff.数据库

发布用户动态

让咱们从简单的事情开始吧。 首页须要有一个表单,用户能够在其中键入新动态。 我建立一个表单类:flask

app / forms.py:博客提交表单。
1
class PostForm(FlaskForm): 2 post = TextAreaField('Say something', validators=[ 3 DataRequired(), Length(min=1, max=140)]) 4 submit = SubmitField('Submit')

 

而后,我将该表单添加到网站首页的模板中:浏览器

app / templates / index.html:索引模板中的提交表单 
1
{% extends "base.html" %} 2 3 {% block content %} 4 <h1>Hi, {{ current_user.username }}!</h1> 5 <form action="" method="post"> 6 {{ form.hidden_tag() }} 7 <p> 8 {{ form.post.label }}<br> 9 {{ form.post(cols=32, rows=4) }}<br> 10 {% for error in form.post.errors %} 11 <span style="color: red;">[{{ error }}]</span> 12 {% endfor %} 13 </p> 14 <p>{{ form.submit() }}</p> 15 </form> 16 {% for post in posts %} 17 <p> 18 {{ post.author.username }} says: <b>{{ post.body }}</b> 19 </p> 20 {% endfor %} 21 {% endblock %}

 

 

模板中的变动和处理之前的表单相似。最后的部分是将表单处理逻辑添加到视图函数中:网络

 app / routes.py:在索引视图功能中发布提交表单。
1
from app.forms import PostForm 2 from app.models import Post 3 4 @app.route('/', methods=['GET', 'POST']) 5 @app.route('/index', methods=['GET', 'POST']) 6 @login_required 7 def index(): 8 form = PostForm() 9 if form.validate_on_submit(): 10 post = Post(body=form.post.data, author=current_user) 11 db.session.add(post) 12 db.session.commit() 13 flash('Your post is now live!') 14 return redirect(url_for('index')) 15 posts = [ 16 { 17 'author': {'username': 'John'}, 18 'body': 'Beautiful day in Portland!' 19 }, 20 { 21 'author': {'username': 'Susan'}, 22 'body': 'The Avengers movie was so cool!' 23 } 24 ] 25 return render_template("index.html", title='Home Page', form=form, 26 posts=posts)

 

 

咱们来一个个地解读该视图函数的变动:session

  • 导入PostPostForm
  • 关联到index视图函数的两个路由都新增接受POST请求,以便视图函数处理接收的表单数据
  • 处理表单的逻辑会为post表插入一条新的数据
  • 模板新增接受form对象,以便渲染文本输入框

在继续以前,我想提一些与Web表单处理相关的重要内容。 请注意,在处理表单数据后,我经过发送重定向到主页来结束请求。 我能够轻松地跳太重定向,并容许函数继续向下进入模板渲染部分,由于这已是主页视图函数了。app

那么,为何重定向呢? 经过重定向来响应Web表单提交产生的POST请求是一种标准作法。 这有助于缓解在Web浏览器中执行刷新命令的烦恼。 当你点击刷新键时,全部的网页浏览器都会从新发出最后的请求。 若是带有表单提交的POST请求返回一个常规的响应,那么刷新将从新提交表单。 由于这不是预期的行为,因此浏览器会要求用户确认重复的提交,可是大多数用户却很难理解浏览器询问的内容。不过,若是一个POST请求被重定向响应,浏览器如今被指示发送GET请求来获取重定向中指定的页面,因此如今最后一个请求再也不是’POST’请求了, 刷新命令就能以更可预测的方式工做。模块化

这个简单的技巧叫作Post/Redirect/Get模式。 它避免了用户在提交网页表单后无心中刷新页面时插入重复的动态。

展现用户动态

若是你还记得,我建立过几条模拟的用户动态,展现在主页已经有一段时间了。 这些模拟对象是在index视图函数中显式建立的一个简单的Python列表:

 1 posts = [
 2         { 
 3             'author': {'username': 'John'}, 
 4             'body': 'Beautiful day in Portland!' 
 5         },
 6         { 
 7             'author': {'username': 'Susan'}, 
 8             'body': 'The Avengers movie was so cool!' 
 9         }
10     ]

 

可是如今我在User模型中有了followed_posts()方法,它能够返回给定用户但愿看到的用户动态的查询结果集。 因此如今我能够用真正的用户动态替换模拟的用户动态:

app / routes.py:在首页中显示真实帖子。
1
@app.route('/', methods=['GET', 'POST']) 2 @app.route('/index', methods=['GET', 'POST']) 3 @login_required 4 def index(): 5 # ... 6 posts = current_user.followed_posts().all() 7 return render_template("index.html", title='Home Page', form=form, 8 posts=posts)

 

User类的followed_posts方法返回一个SQLAlchemy查询对象,该对象被配置为从数据库中获取用户感兴趣的用户动态。 在这个查询中调用all()会触发它的执行,返回值是包含全部结果的列表。 因此我最终获得了一个与我迄今为止一直使用的模拟用户动态很是类似的结构。 它们很是接近,模板甚至不须要改变。

更容易地发现和关注用户

相信你已经留意到了,应用没有一个很好的途径来让用户能够找到其余用户进行关注。实际上,如今根本没有办法在页面上查看到底有哪些用户存在。我将会使用少许简单的变动来解决这个问题。

我将会建立一个新的“发现”页面。该页面看起来像是主页,可是却不是只显示已关注用户的动态,而是展现全部用户的所有动态。新增的发现视图函数以下:

app / routes.py:浏览视图功能。
1
@app.route('/explore') 2 @login_required 3 def explore(): 4 posts = Post.query.order_by(Post.timestamp.desc()).all() 5 return render_template('index.html', title='Explore', posts=posts)

 

你有没有注意到这个视图函数中的奇怪之处? render_template()引用了我在应用的主页面中使用的index.html模板。 这个页面与主页很是类似,因此我决定重用这个模板。 但与主页不一样的是,在发现页面不须要一个发表用户动态表单,因此在这个视图函数中,我没有在模板调用中包含form参数。

要防止index.html模板在尝试呈现不存在的Web表单时崩溃,我将添加一个条件,只在传入表单参数后才会呈现该表单:

app / templates / index.html:使博客文章提交表单为可选。
1
{% extends "base.html" %} 2 3 {% block content %} 4 <h1>Hi, {{ current_user.username }}!</h1> 5 {% if form %} 6 <form action="" method="post"> 7 ... 8 </form> 9 {% endif %} 10 ... 11 {% endblock %}

 

该页面也须要添加到导航栏中:

app / templates / base.html:连接到导航栏中的浏览页面。
1
<a href="{{ url_for('explore') }}">Explore</a>

 

还记得我在第六章中介绍的用于我的主页渲染用户动态的_post.html子模板吗? 这是一个包含在我的主页模板中的小模板,它独立于其余模板,所以也能够被这些模板调用。 我如今要作一个小小的改进,将用户动态做者的用户名显示为一个连接:

app / templates / _post.html:在博客文章中显示做者连接。
1
<table> 2 <tr valign="top"> 3 <td><img src="{{ post.author.avatar(36) }}"></td> 4 <td> 5 <a href="{{ url_for('user', username=post.author.username) }}"> 6 {{ post.author.username }} 7 </a> 8 says:<br>{{ post.body }} 9 </td> 10 </tr> 11 </table>

 

而后在主页和发现页中使用这个子模板来渲染用户动态:

app / templates / index.html:使用博客文章子模板
1
... 2 {% for post in posts %} 3 {% include '_post.html' %} 4 {% endfor %} 5 ...

 

子模板指望存在一个名为post的变量,才能正常工做。该变量是上层模板中经过循环产生的。

经过这些细小的变动,应用的用户体验获得了大大的提高。如今,用户能够访问发现页来查看陌生用户的动态,并经过这些用户动态来关注用户,而须要的操做仅仅是点击用户名跳转到其我的主页并点击关注连接。使人叹为观止!对吧?

此时,我建议你在应用上再次尝试一下这个功能,以便体验最后的用户接口的完善。

用户动态

用户动态的分页

应用看起来更完善了,可是在主页显示全部用户动态早晚会出问题。若是一个用户有成千上万条关注的用户动态时,会发生什么?你能够想象获得,管理这么大的用户动态列表将会变得至关缓慢和低效。

为了解决这个问题,我会将用户动态进行分页。这意味着一开始显示的只是全部用户动态的一部分,并提供连接来访问其他的用户动态。Flask-SQLAlchemy的paginate()方法原生就支持分页。例如,我想要获取用户关注的前20个动态,我能够将all()结束调用替换成以下的查询:

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

 

Flask-SQLAlchemy的全部查询对象都支持paginate方法,须要输入三个参数来调用它:

  • 从1开始的页码
  • 每页的数据量
  • 错误处理布尔标记,若是是True,当请求范围超出已知范围时自动引起404错误。若是是False,则会返回一个空列表。

paginate方法返回一个Pagination的实例。其items属性是请求内容的数据列表。Pagination实例还有一些其余用途,我会在以后讨论。

如今想一想如何在index()视图函数展示分页呢。我先来给应用添加一个配置项,以表示每页展现的数据列表长度吧。

config.py:每页配置数。
1
class Config(object): 2 # ... 3 POSTS_PER_PAGE = 3

 

存储这些应用范围的“可控机关”到配置文件是一个好主意,由于这样我调整时只需去一个地方。 在最终的应用中,每页显示的数据将会大于三,可是对于测试而言,使用小数字很方便。

接下来,我须要决定如何将页码并入到应用URL中。 一个至关常见的方法是使用查询字符串参数来指定一个可选的页码,若是没有给出则默认为页面1。 如下是一些示例网址,显示了我将如何实现这一点:

要访问查询字符串中给出的参数,我可使用Flask的request.args对象。 你已经在第五章中看到了这种方法,我用Flask-Login实现了用户登陆的能够包含一个next查询字符串参数的URL。

给主页和发现页的视图函数添加分页的代码变动以下:

 app / routes.py:追随者关联表
1
@app.route('/', methods=['GET', 'POST']) 2 @app.route('/index', methods=['GET', 'POST']) 3 @login_required 4 def index(): 5 # ... 6 page = request.args.get('page', 1, type=int) 7 posts = current_user.followed_posts().paginate( 8 page, app.config['POSTS_PER_PAGE'], False) 9 return render_template('index.html', title='Home', form=form, 10 posts=posts.items) 11 12 @app.route('/explore') 13 @login_required 14 def explore(): 15 page = request.args.get('page', 1, type=int) 16 posts = Post.query.order_by(Post.timestamp.desc()).paginate( 17 page, app.config['POSTS_PER_PAGE'], False) 18 return render_template("index.html", title='Explore', posts=posts.items)

 

经过这些更改,这两个路由决定了要显示的页码,能够从page查询字符串参数得到或是默认值1。而后使用paginate()方法来检索指定范围的结果。 决定页面数据列表大小的POSTS_PER_PAGE配置项是经过app.config对象中获取的。

请注意,这些更改很是简单,每次更改都只会影响不多的代码。 我试图在编写应用每一个部分的时候,不作任何有关其余部分如何工做的假设,这使我能够编写更易于扩展和测试的且兼具模块化和健壮性的应用,而且不太可能失败或出现BUG。

来尝试下分页功能吧。 首先确保你有三条以上的用户动态。 在发现页面中更方便测试,由于该页面显示全部用户的动态。 你如今只会看到最近的三条用户动态。 若是你想看接下来的三条,请在浏览器的地址栏中输入http://localhost:5000/explore?page=2

分页导航

接下来的改变是在用户动态列表的底部添加连接,容许用户导航到下一页或上一页。 还记得我曾提到过paginate()的返回是Pagination类的实例吗? 到目前为止,我已经使用了此对象的items属性,其中包含为所选页面检索的用户动态列表。 可是这个分页对象还有一些其余的属性在构建分页连接时颇有用:

  • has_next: 当前页以后存在后续页面时为真
  • has_prev: 当前页以前存在前置页面时为真
  • next_num: 下一页的页码
  • prev_num: 上一页的页码

有了这四个元素,我就能够生成上一页和下一页的连接并将其传入模板以渲染:

app / routes.py:下一页和上一页连接。
1
@app.route('/', methods=['GET', 'POST']) 2 @app.route('/index', methods=['GET', 'POST']) 3 @login_required 4 def index(): 5 # ... 6 page = request.args.get('page', 1, type=int) 7 posts = current_user.followed_posts().paginate( 8 page, app.config['POSTS_PER_PAGE'], False) 9 next_url = url_for('index', page=posts.next_num) \ 10 if posts.has_next else None 11 prev_url = url_for('index', page=posts.prev_num) \ 12 if posts.has_prev else None 13 return render_template('index.html', title='Home', form=form, 14 posts=posts.items, next_url=next_url, 15 prev_url=prev_url) 16 17 @app.route('/explore') 18 @login_required 19 def explore(): 20 page = request.args.get('page', 1, type=int) 21 posts = Post.query.order_by(Post.timestamp.desc()).paginate( 22 page, app.config['POSTS_PER_PAGE'], False) 23 next_url = url_for('explore', page=posts.next_num) \ 24 if posts.has_next else None 25 prev_url = url_for('explore', page=posts.prev_num) \ 26 if posts.has_prev else None 27 return render_template("index.html", title='Explore', posts=posts.items, 28 next_url=next_url, prev_url=prev_url)

 

这两个视图函数中的next_urlprev_url只有在该方向上存在一个页面时,才会被设置为由url_for()返回的URL。 若是当前页面位于用户动态集合的末尾或者开头,那么Pagination实例的has_nexthas_prev属性将为’False’,在这种状况下,将设置该方向的连接为None

url_for()函数的一个有趣的地方是,你能够添加任何关键字参数,若是这些参数的名字没有直接在URL中匹配使用,那么Flask将它们设置为URL的查询字符串参数。

如今让咱们把它们渲染在index.html模板上,就在用户动态列表的正下方:

app / templates / index.html:在模板上呈现分页连接。
1
... 2 {% for post in posts %} 3 {% include '_post.html' %} 4 {% endfor %} 5 {% if prev_url %} 6 <a href="{{ prev_url }}">Newer posts</a> 7 {% endif %} 8 {% if next_url %} 9 <a href="{{ next_url }}">Older posts</a> 10 {% endif %} 11 ...

 

主页和发现页都添加了分页连接。第一个连接标记为“Newer posts”,并指向前一页(请记住,我显示的用户动态按时间的倒序来排序,因此第一页是最新的内容)。 第二个连接标记为“Older posts”,并指向下一页的帖子。 若是这两个连接中的任何一个都是None,则经过条件过滤将其从页面中省略。

分页

我的主页中的分页

主页分页已经完成,可是,我的主页中也有一个用户动态列表,其中只显示我的主页拥有者的动态。 为了保持一致,我的主页也应该实现分页,以匹配主页的分页样式。

我开始更新我的主页视图函数,其中仍然有一个模拟用户动态的列表。

app / routes.py:用户我的资料视图功能中的分页。
1
@app.route('/user/<username>') 2 @login_required 3 def user(username): 4 user = User.query.filter_by(username=username).first_or_404() 5 page = request.args.get('page', 1, type=int) 6 posts = user.posts.order_by(Post.timestamp.desc()).paginate( 7 page, app.config['POSTS_PER_PAGE'], False) 8 next_url = url_for('user', username=user.username, page=posts.next_num) \ 9 if posts.has_next else None 10 prev_url = url_for('user', username=user.username, page=posts.prev_num) \ 11 if posts.has_prev else None 12 return render_template('user.html', user=user, posts=posts.items, 13 next_url=next_url, prev_url=prev_url)

 

为了获得用户的动态列表,我利用了User模型中已经定义好的user.posts一对多关系。 我执行该查询并添加一个order_by()子句,以便我首先获得最新的用户动态,而后彻底按照我对主页和发现页面中的用户动态所作的那样进行分页。 请注意,由url_for()函数生成的分页连接须要额外的username参数,由于它们指向我的主页,我的主页依赖用户名做为URL的动态组件。

最后,对user.html模板的更改与我在主页上所作的更改相同:

app / templates / user.html:用户我的资料模板中的分页连接。
1
... 2 {% for post in posts %} 3 {% include '_post.html' %} 4 {% endfor %} 5 {% if prev_url %} 6 <a href="{{ prev_url }}">Newer posts</a> 7 {% endif %} 8 {% if next_url %} 9 <a href="{{ next_url }}">Older posts</a> 10 {% endif %}

 

完成对分页功能的实验后,能够将POSTS_PER_PAGE配置项设置为更合理的值:

config.py:每页配置数。
1
class Config(object): 2 # ... 3 POSTS_PER_PAGE = 25
相关文章
相关标签/搜索