加载中

This is the ninth article in the series in which I document my experience writing web applications in Python using the Flask microframework.

The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to callmicroblog.

Here is an index of all the articles in the series that have been published to date:

Recap

In the previous article in the series we've made all the database changes necessary to support the 'follower' paradigm, where users choose other users to follow.

Today we will build on what we did last time and enable our application to accept and deliver real content to its users. We are saying goodbye to the last of our fake objects today!

概述

前一篇文章中,我们已经取得了所有必要的支持“跟随”模式,让用户选择其他用户对数据库的修改。

今天,我们将说说上一次我们做了什么,以使我们的应用程序可以接收和发送真实信息给用户。我们今天将对我们最后的虚假消息说再见!

Submission of blog posts

Let's start with something simple. The home page should have a form for users to submit new posts.

First we define a single field form object (fileapp/forms.py):

class PostForm(Form):
    post = TextField('post', validators = [Required()])

Next, we add the form to the template (fileapp/templates/index.html):

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

Nothing earth shattering so far, as you can see. We are simply adding yet another form, like the ones we've done before.

Blog Posts的提交

让我们从简单的开始。首页上必须有一张用户提交新的post的表单。

首先我们定义一个单域表单对象(fileapp/forms.py):

class PostForm(Form):
    post = TextField('post', validators = [Required()])

下面,我们把这个表单添加到template中(fileapp/templates/index.html):

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

到目前为止没啥新的东西,你可以看到,我们仅仅添加了另一表单,就像我们上一次做的那样。

Last of all, the view function that ties everything together is expanded to handle the form (fileapp/views.py):

from forms import LoginForm, EditForm, PostForm
from models import User, ROLE_USER, ROLE_ADMIN, 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)

Let's review the changes in this function one by one:

  • We are now importing thePostandPostFormclasses
  • We accept POST requests in both routes associated with theindexview function, since that is how we will receive submitted posts.
  • When we arrive at this view function through a form submission we insert a newPostrecord into the database. When we arrive at it via a regular GET request we do as before.
  • The template now receives an additional argument, theform, so that it can render the text field.

最后,功能试图把所有东西都联系在一起,并被扩展来处理这个表单(fileapp/views.py):

from forms import LoginForm, EditForm, PostForm
from models import User, ROLE_USER, ROLE_ADMIN, 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类
  • 我们接收了来自两个路径下的index和视图的POST请求,因为那就是我们如何接收提交的请求。
  • 当我们通过表单提交到功能视图后,我们会把新的Post记录录入数据库。然后就像之前做的一样,通过常规的GET请求来访问它。
  • Templat会收到一条额外的内容--表单,所以它会提交给文本域。
One final comment before we continue. Notice how after we insert a newPostinto the detabase we do this:

return redirect(url_for('index'))

We could have easily skipped the redirect and allowed the function to continue down into the template rendering part, and it would have been more efficient. Because really, all the redirect does is return to this same view function to do that, after an extra trip to the client web browser.

So, why the redirect? Consider what happens after the user writes a blog post, submits it and then hits the browser's refresh key. What will the refresh command do? Browsers resend the last issued request as a result of a refresh command.

在我们继续之前还有最后一点提醒:注意下面我们如何添加一条新的Post请求到数据库中:

return redirect(url_for('index'))

我们可以很容易的跳过重定向,并且允许它跳到模板渲染部分,而且效率更高。因为所做的所有重定向在经过web浏览器之后,都返回到这个相同的功能视图中来。

所以,为什么选择重定向?考虑到当用户写下一个blog post请求之后,它只需提交然后点击浏览器刷新按钮。“Refresh”命令能做什么呢?浏览器会重新发送最后发布的请求作为一个“Refresh”命令的结果。(译者注:由于个人水平有限,如果您发现译处与原文有出入敬请指正。谢谢!)

Without the redirect, the last request is the POST request that submitted the form, so a refresh action will resubmit the form, causing a secondPostrecord that is identical to the first to be written to the database. Not good.

By having the redirect, we force the browser to issue another request after the form submission, the one that grabs the redirected page. This is a simple GET request, so a refresh action will now repeat the GET request instead of submitting the form again.

This simple trick avoids inserting duplicate posts when a user inadvertently refreshes the page after submitting a blog post.

如果没有重定向,那么最后提交给表单的就是POST请求,所以一个“Refresh Action”会重新提交那个表单,将会导致第二次提交的post记录和第一次写入数据库中的是相同的。这样的行为Not so good.

若是有了重定向,我们可以强制浏览器在表单提交之后发出另一个请求,它抓取了重定向的页面。这是一个简单的“GET”请求,所以“Refresh”动作会重复“GET”请求而不是再次提交表单。

这个简单的小技巧避免用户在提交一个blog post请求之后,不小心刷新页面导致重复写入post请求。

Displaying blog posts

And now we get to the fun part. We are going to grab blog posts from the database and display them.

If you recall from a few articles ago, we created a couple of fake posts and we've been displaying those in our home page for a long time. The fake objects were created explicitly in theindexview function as a simply Python list:

posts = [
        { 
            'author': { 'nickname': 'John' }, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': { 'nickname': 'Susan' }, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]

But in the last article we created the query that allows us to get all the posts from followed users, so now we can simply replace the above with this (fileapp/views.py):

posts = g.user.followed_posts().all()

And when you run the application you will be seeing blog posts from the database!

展现blog post请求

下面我们来说点有意思的东西。我们要从数据库中抓取blog post请求并失之显示。

如果你回忆一下之前部分文章,我们曾创建了许多所谓“虚假的”的请求并且在首页上面显示了很长时间。这些“虚假的”对象是作为Python list在索引视图中创建的。

posts = [
        { 
            'author': { 'nickname': 'John' }, 
            'body': 'Beautiful day in Portland!' 
        },
        { 
            'author': { 'nickname': 'Susan' }, 
            'body': 'The Avengers movie was so cool!' 
        }
    ]

但是在上一篇文章中,我们创建的查询语句允许我们从“关注的人”当中获取所有的请求,所以我们可以用下面的这个语句来替换上文(fileapp/views.py):

posts = g.user.followed_posts().all()

然后当你运行这个应用的时候,你将会看到冲数据库中抓取到的bolg post请求。

Thefollowed_postsmethod of theUserclass returns a sqlalchemy query object that is configured to grab the posts we are interested in. Callingall()on this query just retrieves all the posts into a list, so we end up with a structure that is very much alike the fake one we've been using until now. It's so close that the template does not even notice.

At this point feel free to play with the application. You can create a few users, make them follow others, and finally post some messages to see how each user sees its blog post stream.

User类的followed_posts方法返回了一条抓取我们感兴趣请求的SQL查询语句。在这个查询语句中,Callingall()检索所有的请求到一个list当中,所以我们以这个很像我们一直沿用至今的“虚假”请求的结构结束。他们如此的相像甚至template都没有注意到。

此时您可以在此应用上自由发挥。你可以创建多个用户,让他们follow其他人,然后发布一些信息来看每一个用户是如何看到它的bolg post请求数据流的。

Pagination

The application is looking better than ever, but we have a problem. We are showing all of the followed posts in the home page. What happens if a user has a thousand followed posts? Or a million? As you can imagine, grabbing and handling such a large list of objects will be extremely inefficient.

Instead, we are going to show this potentially large number of posts in groups, or pages.

分页


我们的程序是越来越像样了,但是我们面临另外一个问题。我们在首页显示了所有的followed post。如果一个用户有上千篇followed post将会发生什么情况?或者一百万篇?就像我们可以想象到的,抓取并处理这么庞大的对象列表是十分低效率的。

我们可以显示把这么大量的post分组来显示,或者分页。

Flask-SQLAlchemy comes with very good support for pagination. If for example, we wanted to get the first three followed posts of some user we can do this:

posts = g.user.followed_posts().paginate(1, 3, False).items

Thepaginatemethod can be called on any query object. It takes three arguments:

  • the page number, starting from 1,
  • the number of items per page,
  • an error flag. If True, when an out of range page is requested a 404 error will be automatically returned to the client web browser. If False, an empty list will be returned instead of an error.

Flask-SQLAlchemy可以很好的支持分页。例如,我们可以通过如下方法,轻松获取某个用户的前3篇的followed posts:

posts = g.user.followed_posts().paginate(1, 3, False).items
分页方法可以被任何query对象调用。它接受3个参数:
  • 页码,从1开始
  • 每页显示的记录数
  • 错误标记。如果是True,如果接收到超出记录范围的页面请求,那么404错误请求将会自动反馈到客户端浏览器。如果是False,那么一个空列表将会被返回,而不显示错误。


返回顶部
顶部