Flask Mega-Tutorial 中文教程 V2.0 第9章:分页

最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。

这是Flask Mega-Tutorial系列的第九章,其中我将告诉您如何对数据库列表进行分页。

供您参考,以下是本系列文章的列表。

第8章中,我进行了一些必要的数据库更改,以支持社交网络非常流行的“关注”功能。有了这个功能,我已经准备好删除开始时放置的最后一块脚手架,模拟用户帖子。在本章中,应用程序将开始接受用户的博客帖子,并在主页和个人资料页面中提供这些博客文章。

本章的GitHub链接是:BrowseZipDiff


发表博客文章

让我们从简单的事情开始吧。主页需要有一个表单,用户可以在其中键入新帖子。首先,我创建一个表单类:

# app/forms.py: Blog submission form.

class PostForm(FlaskForm):
    post = TextAreaField('Say something', validators=[
        DataRequired(), Length(min=1, max=140)])
    submit = SubmitField('Submit')

接下来,我可以将此表单添加到应用程序主页的模板中:

app/templates/index.html: Post submission form in index template

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ 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 %}

此模板中的更改与以前的表单处理方式类似。最后一部分是添加表单创建和处理逻辑到视图函数中:

# app/routes.py: Post submission form in index view function.

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)

让我们逐一解读此视图​​函数中的更改:

  • 导入Post和PostForm
  • 关联index视图函数的两个路由除了GET都新增POST请求,以便此视图函数处理接收的表单数据。
  • 表单处理逻辑将在数据库中插入一条新的post记录。
  • 模板接收新增form对象,以便渲染文本输入框。

在继续之前,我想提一些与处理Web表单相关的重要事项。请注意,在处理表单数据后,我通过重定向到主页来结束请求。我可以轻松地跳过重定向,并允许函数继续向下进入模板渲染部分,因为这已经是主页视图函数了。

那么,为什么重定向呢?标准做法是使用重定向响应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: Display real posts in home page.

@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()会触发其执行,返回值为包含所有结果的列表。所以我最终得到一个与我迄今为止使用过的模拟帖子非常相似的结构。它们非常接近,模板甚至不需要改变。

更容易找到要关注的用户

相信你已经留意到了,应用程序并没有一个很好的途径来让用户找到其他用户来进行关注。事实上,现在根本没有办法查看有哪些用户存在。我将通过一些简单的更改来解决这个问题。

我要创建一个新页面,称之为“Explore”页面。此页面看起来像主页,但却不会仅显示来自已关注用户的帖子,而是显示来自所有用户的全部帖子列表。新的浏览视图函数如下:

# app/routes.py: Explore view function.

@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模板。由于这个页面与主页面非常相似,所以我决定重用该模板。但与主页面的一个区别是,在Explore页面中我不需要一个表单来写博客帖子,所以在这个视图函数中我没有在模板调用中包含form参数。

为了防止index.html模板在尝试渲染不存在的Web表单时崩溃,我将添加一个条件,只有在传入的参数不为空时才渲染表单:

app/templates/index.html: Make the blog post submission form optional

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% if form %}
    <form action="" method="post">
        ...
    </form>
    {% endif %}
    ...
{% endblock %}

这个页面也需要添加到导航栏中:

app/templates/base.html: Link to explore page in navigation bar.

<a href="{{ url_for('explore') }}">Explore</a>

还记得我在第6章介绍的用于在用户个人资料页面中渲染博客帖子的_post.html子模板吗?这是一个包含在用户配置页面模板中的小模板,它独立于其它模板,因此也可以被其他模板中调用。我现在要对它进行一些改进,将博客文章作者的用户名显示为一个链接:

app/templates/_post.html: Show link to author in blog posts.

    <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>

我现在可以使用此子模板在主页和浏览页面渲染博客帖子:

app/templates/index.html: Use blog post sub-template.

    ...
    {% for post in posts %}
        {% include '_post.html' %}
    {% endfor %}
    ...

子模板需要存在一个post变量,才能正常工作。该变量是index模板的循环变量的命名方式。

通过这些微小的变更,应用程序的用户体验得到了显着改善。现在,用户可以访问探索页面以阅读来自陌生用户的博客帖子,并根据这些帖子找到要关注的新用户,只需单击用户名即可访问该个人资料页面。太棒了,不是吗?

此时我建议您再次尝试该应用程序,以便体验这些最后的用户界面改进。

ch09-explore

博客帖子的分页

该应用程序看起来更完善了,但显示主页中所有已关注用户的帖子将很快成为一个问题。如果用户有成千上万条关注的用户帖子,会发生什么呢?如果一百万呢?可以想象,管理如此庞大的帖子列表将非常缓慢且低效。

为了解决这个问题,我打算对帖子列表进行分页。这意味着,一开始我只显示有限数量的帖子,并包含用于浏览整个帖子列表的链接。Flask-SQLAlchemy本身支持使用paginate()查询方法进行分页。例如,如果我想获得用户的前20个帖子,我可以用以下内容替换all()终止查询的调用:

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

可以在Flask-SQLAlchemy的任何查询对象上调用paginate方法。它需要三个参数:

  • 页码,从1开始
  • 每页的显示列表长度
  • 错误标志。如果True,当请求超出范围的页面时,404错误将自动返回给客户端。如果False,则会返回一个空列表。

paginate返回值是一个Pagination对象。此对象的items属性包含所请求页面中的项目列表。我将在稍后讨论Pagination对象中的其它一些用途。

现在让我们考虑如何在index()视图函数中实现分页。我可以首先向应用程序添加一个配置项,以确定每页显示的项目列表长度。

# config.py: Posts per page configuration.

class Config(object):
    # ...
    POSTS_PER_PAGE = 3

保存这些应用范围的“可控变量”到配置文件是一个好主意,因为这样我调整时只需去一个地方。在最终的应用程序中,我当然让每页显示的列表长度大于3,但是对于测试,使用小数字更方便。

接下来,我需要决定如何将页码合并到应用程序的URL中。一种相当常见的方法是使用查询字符串参数来指定可选的页码,如果没有给出,则默认为第1页。以下是一些示例网址,展示了我如何实现这一点:

  • 第1页,隐含:http://localhost:5000/index
  • 第1页,显式: http://localhost:5000/index?page=1
  • 第3页: http://localhost:5000/index?page=3

要访问查询字符串中给出的参数,我可以使用Flask的request.args对象。您已在第5章中看到过这一点,在其中我用Flask-Login中实现了用户登录的可包含一个next查询字符串参数的URL 。

下面你可以看到我如何在index和explore视图函数添加分页:

# app/routes.py: Followers association table

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
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', 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()方法仅检索所需的结果页面。决定页面大小的POSTS_PER_PAGE配置项是通过app.config对象中获取的。

请注意这些更改是非常容易,以及每次更改时对代码的影响都很小。我正在尝试编写应用程序每个部分的时候,不对其他部分如何工作做出任何假设,这使我能够编写更易于扩展和测试且兼具模块化和健壮性的应用程序,并且不太可能出现故障或bugs。

尝试一下分页功能。首先要确保你有三篇以上的博文。在浏览页面中更方便测试,因为这个页面显示了所有用户的帖子。您现在只会看到最近的三篇帖子。如果要查看接下来的三篇,请在浏览器的地址栏中键入 http://localhost:5000/explore?page=2 

页面导航

下一处更改是在博客帖子列表底部添加链接,允许用户导航到next和/或previous。还记得我提到过,调用paginate()的返回值是 Flask-SQLAlchemy中Pagination类的对象?到目前为止,我已经使用了此对象的items属性,其中包含为所选页面检索的用户帖子列表。但是,Pagination对象还有一些其他的属性在构建分页链接时很有用:

  • has_next:如果当前页面后面至少还有一页,则为True
  • has_prev:如果在当前页面之前至少还有一页,则为True
  • next_num:下一页的页码
  • prev_num:上一页的页码

通过这四个元素,我可以生成上一页和下一页链接,并将它们传递给模板进行渲染:

# app/routes.py: Next and previous page links.

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
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', 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_urlprev_url通过url_for()设置为返回的URL 。如果当前页面位于帖子列表的末尾或者开头,则Pagination对象的属性has_nexthas_prev属性将是False,并且在这种情况下,上一页或者下一页方向上的链接将被设置为None

我之前没有讨论的url_for()函数的一个有趣的地方是你可以向它添加任何关键字参数,如果这些参数的名称没有直接在URL中引用,那么Flask会将它们作为查询字符串参数包含在URL中。

分页链接被设置在index.html模板中,所以现在让我们在页面帖子列表的正下方渲染它们:

app/templates/index.html: Render pagination links on the template.

    ...
    {% 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 %}
    ...

此更改会在首页和探索页面上的帖子列表下添加链接。第一个链接标记为“Newer posts”,它指向上一页(请记住,我显示的帖子是按他们创建的时间倒序排列的,因此第一页是最新的帖子)。第二个链接标记为“Older posts”,并指向帖子的下一页。如果这两个链接中的任何一个是None,则通过条件过滤将其从页面中省略。

ch09-pagination

用户个人资料页面中的分页

首页页面的更改已完成。但是,用户个人资料页面中也有一个帖子列表,其中仅显示来自个人资料拥有者的帖子。为了保持一致,应更改用户个人资料页面,以匹配首页页面的分页样式。

我首先更新用户个人资料视图功能,其中仍然是一个模拟用户的帖子列表。

# app/routes.py: Pagination in the user profile view function.

@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)

为了获取用户的帖子列表,我利用了SQLAlchemy根据User模型中的db.relationship()定义而设置的关系查询user.posts。我使用这个查询并添加一个order_by()子句,以便我先获得最新的帖子,然后像我对首页和探索页面中的帖子那样进行分页。请注意,url_for()函数生成的分页链接需要额外的username参数,因为它们指向用户个人资料页面,这个页面依赖用户名作为URL的动态参数。

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

app / templates / user.html:用户个人资料模板中的分页链接。

<!-- app/templates/user.html: Pagination links in the user profile template. -->
    ...
    {% 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 %}

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

# config.py: Posts per page configuration.

class Config(object):
    # ...
    POSTS_PER_PAGE = 25

原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-ix-pagination 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值