Python Flask Web框架教程 9 分页

本章节介绍了如何在社交网络博客应用中添加用户提交博客文章的功能,以及如何处理Web表单的Post/Redirect/Get模式以避免重复提交。接着,通过创建浏览页面和改进个人资料页面,使用户能更容易地发现和关注其他用户。最后,通过分页技术优化了首页和浏览页面,以适应大量博客文章,确保了应用的高效性和用户体验。
摘要由CSDN通过智能技术生成

原文

第8章中,我做了一些必要的数据库更改,以支持在社交网络中非常流行的“关注者”范例。 有了该功能之后,我就可以删除在开头放置的最后一块脚手架,即假帖子。 在本章中,该应用程序将开始接受来自用户的博客文章,并在主页和个人资料页面中进行发布。

本章的GitHub链接是:浏览zip差异

提交博客文章

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

# 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 %}

该模板中的更改与以前的表单处理方式相似。 最后一部分是在view函数中添加表单创建和处理:

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

让我们一一回顾一下此视图功能中的更改:

  • 我现在要导入PostPostForm
  • 除了GET请求外,我在与index视图功能关联的两条路由中都接受POST请求,因为该视图功能现在将接收表单数据。
  • 表单处理逻辑将新的Post记录插入数据库。
  • 模板接收form对象作为附加参数,以便它可以呈现文本字段。

在继续之前,我想提到与处理Web表单有关的重要事项。请注意,在处理表单数据之后,如何通过发出重定向到主页来结束请求。我本可以轻松地跳过重定向,并允许该函数继续向下进入模板呈现部分,因为这已经是索引视图函数。

那么,为什么要重定向?这是一种标准做法,它通过重定向来响应由Web表单提交生成的POST请求。这有助于减轻在Web浏览器中如何执行refresh命令的麻烦。当你按下刷新键时,Web浏览器所做的就是重新发出上一个请求。如果提交表单的POST请求返回常规响应,则刷新将重新提交表单。因为这是意外的,所以浏览器将要求用户确认重复提交,但是大多数用户将无法理解浏览器在问他们什么。但是,如果通过重定向答复了POST请求,则现在指示浏览器发送GET请求以获取重定向中指示的页面,因此现在最后一个请求不再是POST请求,并且refresh命令工作在更可预测的方式。

这个简单的技巧称为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模板,我正在应用程序的主页中使用该模板。 由于此页面与主页非常相似,因此我决定重用该模板。 但是与主页的不同之处在于,在浏览页面中,我不希望使用表单来撰写博客文章,因此,在此视图功能中,我没有在模板调用中包含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()查询方法进行分页。 例如,如果我想获得该用户的前二十个帖子,则可以将终止查询的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

拥有这些可以通过配置文件更改应用程序范围的行为的“旋钮”是一个好主意,因为这样我就可以到一个地方进行调整。当然,在最终应用程序中,每页我将使用大于三项的数字,但是对于测试,使用小数字很有用。

接下来,我需要确定如何将页码合并到应用程序URL中。一种相当普遍的方法是使用查询字符串参数来指定可选的页码,如果未指定,则默认为第1页。以下是一些示例URL,它们显示了我将如何实现此目标:

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

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

在下面,你可以看到我如何向主页和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()方法仅检索所需的结果页面。可以通过app.config对象访问确定页面大小的POSTS_PER_PAGE配置项。

注意这些更改有多容易,每次更改对代码的影响都很小。我试图在编写应用程序的任意部分时,不对其他部分的工作方式做任何假设,这使我能够编写模块化和健壮的应用程序,这些应用程序更易于扩展和测试,并且不太可能出现故障或出现错误。

继续尝试分页支持。首先,请确保拥有三个以上的博客文章。这在浏览页面中比较容易看到,该页面显示了所有用户的帖子。现在,你将只看到三个最新的帖子。如果要查看下三个,请在浏览器的地址栏中键入http://localhost:5000/explore?page=2

页面导航

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

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

此更改在索引和浏览页面上的帖子列表下方添加了两个链接。 第一个链接标记为“较新的帖子”,它指向上一页(请记住,我显示的帖子按最新顺序排列,因此第一页是内容最新的页面)。 第二个链接标记为“较旧的帖子”,指向帖子的下一页。 如果这两个链接中的任何一个为“无”,则通过条件将其从页面中省略。

ch09-pagination

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

现在,index页面的更改已足够。 但是,用户个人资料页面中也有一个帖子列表,该列表仅显示该个人资料所有者的帖子。 为了保持一致,应该更改用户个人资料页面以匹配索引页面的分页样式。

首先,更新用户个人资料视图功能,该功能中仍然包含伪造的发布对象列表。

# 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
    form = EmptyForm()
    return render_template('user.html', user=user, posts=posts.items,
                           next_url=next_url, prev_url=prev_url, form=form)

为了从用户那里获取帖子列表,我利用了一个事实,即user.posts关系是由SQLAlchemy设置在用户模型中db.relationship()定义的查询。 我接受此查询并添加了order_by()子句,以便首先获取最新的帖子,然后完全按照对索引和浏览页面中的帖子所做的方式进行分页。 请注意,由url_for()函数生成的分页链接需要额外的username参数,因为它们指向的是用户个人资料页面,该页面具有该用户名作为URL的动态组成部分。

最后,对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

(本章完毕)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值