此文章为译文(简化版,删掉了一些重要性低的部分) 原文在https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world

下面是这个系列的目录

章节 1: Hello, World! 开始

设定你已安装Python3,如果没有安装,请自行搜索安装

安装Flask

下一步是安装Flask

像下面这样, 使用pip命令在你的机器上安装Flask(全局):

$ pip install flask

为解决不同应用程序维护不同版本包的问题, Python使用虚拟环境(virtual environments)概念. 一个虚拟环境是一个Python解释器的完成克隆. 当你在虚拟环境安装包时, 系统(全局)的Python解释器不会收到影响, 只影响该克隆版本. 所以像完全自由的安装每个应用程序的任意版本包可以为每个应用程序使用不同的虚拟环境解决. 虚拟环境有更多的好处, 它有创建它的用户所拥有, 所以它不需要管理员账户

创建一个目录来存放项目. 我给这个目录命名为microblog, 这是应用程序的名称:

$ mkdir microblog
$ cd microblog

如果你使用的是Python3版本, 虚拟环境包含在里面,不需要再安装, 所以你可以直接创建虚拟环境, 运行如下命令:

$ python3 -m venv venv

这个命令含义是: 请求Python运行venv包, 创建一个名为venv的虚拟环境. 命令中的第一个venv是Python虚拟环境包, 第二个是虚拟环境的名称. 如果你感到困惑的话, 你可以替换第二个venv为你的虚拟环境指定不同的名称. 一般我在项目目录下创建名为venv的虚拟环境, 所以每当我cd(进入)到项目中我都能找到相应的虚拟环境.

注意:在一些操作系统你需要使用python代替python3改变上面的命令. 一些使用python命令为Python 2.x版本安装, 使用python3命令为Python 3.x版本安装, 还有的使用python为Python 3.x版本安装.

命令执行后, 你将有一个名叫venv的目录, 虚拟环境文件将保存在内.

如果你使用的是Python 3.4之前的版本(包括2.7), 在这些版本中, 需要安装虚拟环境包. 在创建虚拟环境之前你需要下载和安装一个名叫virtualenv的第三方工具. 当virtualenv安装完成, 你可以使用下面的命令创建虚拟环境:

$ virtualenv venv

无论你使用哪种方法创建虚拟环境, 你都需要激活你创建的虚拟环境. 现在你需要告诉系统你想使用这个虚拟环境, 你要激活它. 使用下面命令激活你的虚拟环境:

$ source venv/bin/activate
(venv) $ _

如果你使用的是微软的Windows命令行工具, 激活命令有一点不同:

$ venv\Scripts\activate
(venv) $ _

当你激活虚拟环境, 你的终端会话配置将改变, 当你运行python命令,存储在虚拟环境的Python解释器将被调用. 通常, 终端提示符将被修改为已激活的虚拟环境的名称. 这些修改都是临时的, 所以当你关闭终端窗口时它不会继续存在. 如果你同时打开多个终端窗口, 他们可以很好的在多个终端运行不同的虚拟环境.

现在你已经创建和激活了虚拟环境, 你可以在虚拟环境安装Flask:

(venv) $ pip install flask

如果你想确认你的虚拟环境是否安装了Flask, 你可以运行python命令, 并在Python解释器中运行下列命令, 如果没有报错即安装成功:

>>> import flask
>>> _

“Hello, World” Flask应用程序

如果你进入Flask website, 你将看到非常简单的应用程序(只有五行代码). 我将不再重复那个简单的例子,而是向您展示一个稍微复杂一点的例子,它将为您编写大型应用程序提供一个良好的基础结构.

应用程序将存在于包中. 在Python, 一个包含 __init__.py 文件的子目录代表一个包, 它可以被导入. 当你导入一个包, __init__.py 文件执行并定义包向外部公开的名称或符号.

让我们来创建一个名叫app的包, 它将承载应用程序. 确保你在microblog 目录, 然后运行下面的命令:

(venv) $ mkdir app

app包内的 __init__.py 文件包含下面代码:

app/__init__.py: Flask 应用程序实例

from flask import Flask

app = Flask(__name__)

from app import routes

上面的代码简单的创建了一个从flask包导入的Flask类的应用程序对象实例. __name__变量传给Flask类(预先定义的变量), 用来设置正在被使用的模块的名称. Flask使用该传递的模块的位置作为开始的点来加载相关联的资源(比如:模板文件) , 我将在章节 2讲到. 然后应用程序导入routes路由模块(现在还不存在).

一方面, 两个名叫app的实体可能看起来令人困惑. app包通过app目录和__init__.py 内的代码定义, 引用的语句是from app import routes. 而另一个app变量在 __init__.py 脚本内定义为一个一个Flask类的实例 , 使得它成为app包的成员.

另一个特性是routes(路由)模块总是在代码底部而不是在顶部导入. 底部导入是循环导入的变通方法, 是Flask应用程序的一个常见问题. 你会看到routes(路由)模块需要在脚本导入已定义的app变量, 所以在底部放一个相互导入避免了两个文件之间相互引用而导致的错误.

所以routes(路由)模块有什么? 路由是应用程序定义不同URL地址的实现. 在Flask, 通过编写Python函数来处理应用程序路由, 名为视图函数(view functions). 视图函数映射一个或多个路由URL地址, 这样Flask就能理解当客户端请求给定的URL时的执行逻辑.

这是你的第一个视图函数, 你需要编写一个新的名叫app/routes.py的模块:

app/routes.py: 主页路由

from app import app

@app.route('/')
@app.route('/index')
def index():
    return "Hello, World!"

这个视图函数非常简单, 它只返回一个字符串问候. 上面两行奇怪的@app.rout是装饰器(decorators), 一个Python语言的特性. 一个装饰器修饰跟随的函数. 装饰器的一个常见模式是使用它们将函数注册为特定事件的回调函数. 在这里, @app.route 装饰器创建一个给定URL参数与函数之间的关联. 在这个例子有两个装饰器, 为URL/ 和/index 与其修饰的函数创建关联. 这意味着当一个web浏览器请求这两个URL时, Flask将调用与它们相关联的函数, 然后返回一个值作为response响应传给浏览器. 如果这还没有完全讲明白的话, 在运行这个程序时, 你可能会明白.

要完成程序, 你需要在Flask应用程序实例的顶级目录定义一个Python脚本. 给这个脚本命名为microblog.py, 并将其定义为导入应用程序实例的单行:

microblog.py: 主要应用程序模块

from app import app

还记得两个app实体吗? 在这里那你可以看到它们在同一个句子里. Flask应用程序实例叫做app, 它是一个名叫app的包的成员. from app import app语句导入app包的成员变量app. 如果你还是感到困惑, 你可以将包或者变量修改为其它名称.

确保你做的所有工作都正确, 下面是目前的项目架构示意图:

microblog/
  venv/
  app/
    __init__.py
    routes.py
  microblog.py

信不信由你, 应用程序的第一个版本已经完成! 在运行它之前, 需要通过设置FLASK_APP环境变量告诉Flask怎么导入它:

(venv) $ export FLASK_APP=microblog.py

如果你使用的是微软的Windows, 使用set替换掉上面命令中的export.

你可以使用下面的命令运行你的第一个web应用程序了:

(venv) $ flask run
 * Serving Flask app "microblog"
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

在服务器初始化之后, 它将等待客户端连接. 运行flask run的输出表示服务器在你的电脑IP地址127.0.0.1上运行. 这个地址的含义是: localhost.(本地) 网络服务监听指定的端口的连接. 应用程序部署在生产web服务器, 通常监听443端口, 如果没有实现加密监听的是80端口, 但是访问这些端口需要管理权限. 因为这个应用程序是在开发环境运行的, Flask使用的是可以自由使用的端口5000. 现在打开你的浏览器并在地址栏输入下面的地址:

    http://localhost:5000/

你也可以使用另一个URL:

    http://localhost:5000/index

您是否看到应用程序路由映射在起作用? 第一个URL映射到/, 第二个URL映射到/index. 这两个路由都只与应用程序内唯一的视图函数关联, 所以它们的输出是一样的(函数返回的字符串). 如果你输入其它的URL你将得到一个错误, 因为只有这两个URL是被应用程序认可的.

Hello, World!

当你想要关闭服务时按Ctrl-C即可

祝贺, 你已经完成了成为web开发者的第一大步!

在结束这个章节前, 我想再提一件事. 因为环境变量不会在终端会话之间被记住, 每当你打开一个新的终端窗口时你要总是要设置FLASK_APP 环境变量. 从版本version 1.0开始, Flask允许你在运行  flask  命令时自动导入注册的环境变量. 你需要安装python-dotenv 包才能使用这个功能:

(venv) $ pip install python-dotenv

然后你可以在项目的顶级目录中的.flaskenv 文件编写环境变量的名称和值:

.flaskenv: flask的环境变量命令

FLASK_APP=microblog.py

这是可选的. 如果你更喜欢手动的设置环境变量, 完全没问题, 只要你记得做就好.

章节 2: Templates 模板

什么是模板?

我希望我的应用程序主页有用户欢迎标题. 我将暂时忽略应用程序还没有用户概念的事实, 因为后面会讲到. 作为替代, 我会使用Python字典实现一个仿制的用户, 如下所示:

user = {'username': 'Miguel'}

创建虚拟对象是有用的技术它可以让你专心于应用程序的部分, 而不是担心还不存在的其它系统问题. 我想设计我应用程序的主页, 我不希望不适合的用户系统让我分心, 所以我只需要创建一个user对象就可以继续了 .

应用程序的视图函数返回一个简单的字符串. 现在我想扩展为返回一个完整的HTML页面字符串, 像这样:

app/routes.py: 从视图函数返回完整的HTML页面

from app import app

@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    return '''
<html>
    <head>
        <title>Home Page - Microblog</title>
    </head>
    <body>
        <h1>Hello, ''' + user['username'] + '''!</h1>
    </body>
</html>'''

如果你不熟悉HTML, 我建议你阅读Wikipedia上的HTML Markup简短介绍.

将视图函数更新为如上所示, 重启应用来看看它在浏览器上的显示.

Mock User

我希望你同意我的观点, 上面交付给浏览器的HTML解决方案是不好的. 考虑一下,当我拥有来自用户的博客文章时,这个视图函数中的代码将变得多么复杂,这些文章将不断地变化 . 应用程序需要更多的视图函数来关联其它的URL, 所以想象如果一天我决定改变应用程序的样式, 需要更新每个视图函数的HTML. 显然,随着应用程序的增长,这不是一个可以伸缩的选项 .

如果您能将应用程序的逻辑与web页面的布局或表示分开,那么事情就会组织得更好,您不这样认为吗? 在用Python编写应用程序逻辑时,您甚至可以雇佣web设计师来创建一个杀手级web站点。

模板有助于实现表示和业务逻辑之间的这种分离。在Flask中,模板被编写为独立的文件,存储在应用程序包内的templates文件夹中。因此,在确保您位于项目目录中之后,创建存储模板的目录:

(venv) $ mkdir app/templates

下面可以看到第一个模板,它的功能类似于上面index()视图函数返回的HTML页面 . 在app/templates/index.html文件编写代码:

app/templates/index.html: 主页模板

<html>
    <head>
        <title>{{ title }} - Microblog</title>
    </head>
    <body>
        <h1>Hello, {{ user.username }}!</h1>
    </body>
</html>

这是一个标准的、非常简单的HTML页面. 这个页面中唯一有趣的事情是,动态内容有两个占位符 , 封装在{{ ... }}部分. 这些占位符表示页面的可变部分,只有在运行时才知道 .

现在页面的表示已经被卸载到HTML模板中,视图函数可以被简化:

app/routes.py: 使用render_template() 函数

from flask import render_template
from app import app

@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    return render_template('index.html', title='Home', user=user)

看起来好多了,对吧?尝试应用程序的这个新版本,看看模板是如何工作的。在浏览器中加载页面之后,您可能希望查看源HTML并将其与原始模板进行比较.

将模板转换为完整HTML页面的操作称为呈现。为了渲染模板,我必须导入一个与烧瓶框架一起提供的函数render_template(). 这个函数接受一个模板文件名和一个模板参数的变量列表,并返回相同的模板,但是其中的所有占位符都被实际值替换。

render_template()函数调用与Flask框架绑定的Jinja2模板引擎. Jinja2用对应的值替换{{ ... }}块, 该值由调用render_template()中提供的参数给出.

条件语句

您已经看到了Jinja2如何在呈现期间用实际值替换占位符,但这只是Jinja2在模板文件中支持的许多强大操作之一. 例如,模板还支持控制语句 , 在{% ... %}块的内部. index.html 模板的下一个版本添加了一个条件语句:

app/templates/index.html: 模板中的条件语句

<html>
    <head>
        {% if title %}
        <title>{{ title }} - Microblog</title>
        {% else %}
        <title>Welcome to Microblog!</title>
        {% endif %}
    </head>
    <body>
        <h1>Hello, {{ user.username }}!</h1>
    </body>
</html>

现在模板变得更智能了。如果视图函数忘记传递title占位符变量的值,那么模板将提供缺省值,而不是显示空标题. 您可以通过删除视图函数的render_template()调用中的title参数来尝试如何使用这个条件.

循环

登录用户可能希望在主页中看到来自已连接用户的最新帖子,所以我现在要做的是扩展应用程序来支持这一点.

再一次,我将依赖于方便的伪对象技巧来创建一些用户和一些帖子来显示:

app/routes.py: 假贴子在视图功能

from flask import render_template
from app import app

@app.route('/')
@app.route('/index')
def index():
    user = {'username': 'Miguel'}
    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', user=user, posts=posts)

为了表示用户帖子,我使用了一个列表,其中每个元素都是一个包含authorbody字段的字典. 当我得到真正实现用户和博客文章我将试着尽可能地保留这些字段名称,这样所有的工作我所做的设计和测试主页模板使用这些假对象将继续有效,当我介绍真实用户和帖子.

在模板方面,我必须解决一个新问题。文章列表可以有任意数量的元素,由视图函数决定在页面中显示多少文章。模板不能对有多少贴子做任何假设,因此需要准备以通用方式呈现视图发送的贴子数量.

对于这类问题,Jinja2提供了一个for控制结构:

app/templates/index.html: for循环的模板

<html>
    <head>
        {% if title %}
        <title>{{ title }} - Microblog</title>
        {% else %}
        <title>Welcome to Microblog</title>
        {% endif %}
    </head>
    <body>
        <h1>Hi, {{ user.username }}!</h1>
        {% for post in posts %}
        <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
        {% endfor %}
    </body>
</html>

简单,是吧?尝试一下这个应用程序的新版本,并确保向posts列表添加更多内容,以查看模板如何适应并始终呈现视图函数发送的所有post.

Mock Posts

模板继承

现在大多数web应用程序在页面顶部都有一个导航栏,其中包含一些常用的链接,比如编辑个人资料、登录、注销等的链接. 我可以使用更多的HTML轻松地将导航栏添加到index.html模板中,但是随着应用程序的增长,我将需要在其他页面中使用相同的导航栏. 我并不想在许多HTML模板中维护导航栏的多个副本,如果可能的话,最好不要重复。

Jinja2有一个模板继承特性,专门解决这个问题。实际上,您所能做的就是将页面布局中所有模板共有的部分移动到一个基本模板中,所有其他模板都是从这个基本模板派生出来的.

因此,我现在要做的是定义一个名为base.html的基本模板,它包含一个简单的导航栏和前面实现的标题逻辑 . 您需要在app/templates/base.html 文件中编写以下模板:

app/templates/base.html: 带有导航栏的基本模板

<html>
    <head>
      {% if title %}
      <title>{{ title }} - Microblog</title>
      {% else %}
      <title>Welcome to Microblog</title>
      {% endif %}
    </head>
    <body>
        <div>Microblog: <a href="/index">Home</a></div>
        <hr>
        {% block content %}{% endblock %}
    </body>
</html>

在这个模板中,我使用了block 控制语句来定义派生模板可以插入自身的位置。block被赋予一个惟一的名称,派生模板在提供其内容时可以引用这个名称.

有了基本模板,我现在可以简化index.html,让它继承自base.html:

app/templates/index.html: 从基模板继承

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}

由于base.html模板现在将处理一般的页面结构,所以我已经从index.html中删除了所有这些元素,只留下了内容部分 . extends语句在两个模板之间建立继承链接,以便Jinja2知道当它被要求呈现index.html时,它需要将其嵌入base.html中. 这两个模板具有与名称内容匹配的块语句,这就是Jinja2如何将两个模板组合成一个模板的原因. 现在,如果我需要为应用程序创建额外的页面,我可以将它们创建为来自相同base.html模板的派生模板,这样我就可以让应用程序的所有页面共享相同的外观,而不需要复制.

Template Inheritance

章节 3: Web Forms web表单

介绍Flask-WTF

为了处理这个应用程序中的web表单,我将使用Flask-WTF扩展, 它是WTForms包的一个薄薄的包装,它很好地集成了Flask. 这是我向你们展示的第一个烧瓶扩展,但不会是最后一个。扩展是Flask生态系统的一个非常重要的部分,因为它们为Flask有意不发表意见的问题提供了解决方案 .

Flask扩展是规则的Pyython包, 可以使用pip安装. 你可以继续在你的虚拟环境安装Flask-WTF:

(venv) $ pip install flask-wtf

配置

到目前为止,应用程序非常简单,因此我不需要担心它的配置。但对于任何应用程序除了最简单的人,你会发现Flask扩展(也可能使用)提供一定数量的自由如何做事情,你需要做出一些决定,通过框架的配置变量的列表.

应用程序有几种指定配置选项的格式。最基本的解决方案是在app.config中将变量定义为键,它使用字典样式来处理变量。例如,你可以这样做 :

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed

虽然上述语法足以为瓶创建配置选项,我喜欢执行关注点分离的原则,而不是把我的配置在同一个地方创建我的申请我将使用一个稍微复杂的结构,允许我保持配置在一个单独的文件.

我非常喜欢的一种格式是使用类来存储配置变量,因为它具有很强的可扩展性。为了保持良好的组织,我将在一个单独的Python模块中创建configuration类。下面您可以看到这个应用程序的新配置类,它存储在顶层目录中的config.py模块中.

config.py: 密钥配置

import os

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

很简单,对吧?配置设置被定义为Config类中的类变量。由于应用程序需要更多的配置项,可以将它们添加到这个类中,稍后如果发现需要多个配置集,可以创建该类的子类。但现在还不用担心.

作为惟一的配置项添加的SECRET_KEY配置变量是大多数Flask应用程序中的一个重要部分。Flask和它的一些扩展使用密钥的值作为密码密钥,这对于生成签名或令牌非常有用. Flask-WTF扩展使用它来保护web表单免受跨站点请求伪造(Cross-Site Request Forgery)或CSRF(发音为“seasurf”)的恶意攻击. 顾名思义,密钥应该是秘密的,因为使用它生成的令牌和签名的强度不依赖于应用程序的可信维护者之外的任何人.

密钥的值被设置为包含两个术语的表达式,由or操作符连接 . 第一项查找环境变量的值,也称为 SECRET_KEY. 第二项,只是一个硬编码的字符串. 对于配置变量,您将经常看到我重复这种模式。其思想是,首选来自环境变量的值,但是如果环境没有定义变量,则使用硬编码字符串。当您开发这个应用程序时,安全性要求很低,所以您可以忽略这个设置,使用硬编码字符串。但是,当这个应用程序部署在生产服务器上时,我将在环境中设置一个惟一且难以猜测的值,这样服务器就有了一个其他人不知道的安全密钥

现在我有一个配置文件, 我需要告诉Flask读取和应用它. 这可以在创建Flask应用程序实例之后使用app.config.from_object()方法:

app/__init__.py: Flask配置

from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

from app import routes

我改进Config类的方式第一次可能看起来令人困惑, 但是如果你查看Flask类(大写字母”F”) 是怎么从flask包导入的(小写字母”f”) 你会注意到我对配置做了相同的处理. 小写”config”是Python模块config.py的名字, 显然大写字母”C”是实际的类.

正如我之前提到的, 配置项可以通过字典的语法从app.config访问. 在这里,您可以看到一个使用Python解释器的快速会话,我在其中查看了密钥的值:

>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'

用户登录表单

Flask-WTF扩展使用Python类来表示web表单. 表单类只是将表单的字段定义为类变量.

再次考虑到关注点的分离,我将使用一个新的app/forms.py模块来存储web表单类. 首先,让我们定义一个用户登录表单,它要求用户输入用户名和密码。表单还将包括一个“记住我”复选框和一个提交按钮:

app/forms.py: 登录表单

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Remember Me')
    submit = SubmitField('Sign In')

大多数Flask扩展对其顶级导入符号使用flask_<name>命名约定. 在本例中,Flask-WTF的所有符号都位于flask_wtf之下. 这就是从app/forms.py顶部导入FlaskForm基类的地方.

由于Flask-WTF扩展不提供定制的版本,所以我为这个表单使用的四个代表字段类型的类直接从WTForms包中导入. 对于每个字段,都在 LoginForm类中创建一个对象作为类变量. 每个字段都有一个描述或标签作为第一个参数.

你在某些字段中看到的可选参数validators,用于将验证行为附加到字段. DataRequired只检查字段是否提交为空。 还有更多可用的验证器,其中一些将以其他形式使用.

Form表单模板

下一步是将表单添加到HTML模板中,以便能够在web页面上呈现. 好消息是,在LoginForm类中定义的字段知道如何将自己呈现为HTML,所以这个任务相当简单。下面您可以看到登录模板,我将把它存储在文件app/templates/login.html:

app/templates/login.html: 登录表单模板

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

对于这个模板,我再次使用章节 2base.html模板作为展示, 通过extends模板继承声明. 实际上,我将对所有模板都这样做,以确保在应用程序的所有页面上都有一个包含顶部导航栏的一致布局 .

该模板期望从LoginForm 类实例化的表单对象作为参数给出,您可以看到作为form引用。这个参数将由login视图函数发送,我还没有编写这个函数.

HTML的<form>元素用作web表单的容器. action属性用于告诉浏览器在提交用户在表单中输入的信息时应该使用的URL . 当操作设置为空字符串时,表单将提交给当前位于地址栏中的URL,即在页面上呈现表单的URL. method 属性指定向服务器提交表单时应使用的HTTP请求方法. 默认使用GET请求发送, 但几乎在所有情况下, 使用POST请求可以提供更好的用户体验,因为这种类型的请求可以在请求体中提交表单数据 , GET请求将表单字段添加到URL,使浏览器地址栏混乱不堪. novalidate 属性用于告诉web浏览器不要对此表单中的字段应用验证,这实际上将此任务留给了在服务器中运行的Flask应用程序 . 使用novalidate 是完全可选的,但是对于第一个表单,设置它是很重要的,因为这将允许您在本章后面测试服务器端验证.

form.hidden_tag()模板参数生成一个隐藏字段,其中包含一个令牌,用于保护表单免受CSRF攻击. 要保护表单,您所需要做的就是包含这个隐藏字段,并在烧瓶配置中定义SECRET_KEY变量. 如果您处理好这两件事,Flask-WTF将为您完成剩下的工作.

如果您曾经编写过HTML web表单,那么您可能会发现这个模板中没有HTML字段,这很奇怪。这是因为表单对象中的字段知道如何将自己呈现为HTML . 所有我需要做的包括{{ form.<field_name>.label }}(我想要的字段标签), 和{{ form.<field_name>() }}(我想要的字段). 对于需要额外HTML属性的字段,可以将它们作为参数传递. 该模板中的用户名和密码字段接受size作为参数,该参数将作为属性添加到 <input>HTML元素中. 这也是您可以将CSS类或id附加到表单字段的方式 .

Form表单视图

在浏览器中查看此表单之前的最后一步是在应用程序中编写一个新的视图函数,该函数将呈现上一节中的模板。

来写一个新的映射/login URL的视图函数来创建一个表单form, 把它传给模板进行渲染. 这个视图函数可以放入之前的app/routes.py模块:

app/routes.py: 登录视图函数

from flask import render_template
from app import app
from app.forms import LoginForm

# ...

@app.route('/login')
def login():
    form = LoginForm()
    return render_template('login.html', title='Sign In', form=form)

我刚才做的是从forms.py 文件导入LoginForm类, 从它实例化一个对象, 将其发送给模板. form=form语法可能看起来奇怪, 但是是简单的传递上面一行创建的form对象 (显示在右侧的)命名为form (显示在左侧的)给模板. 这就是呈现表单字段所需要的全部内容.

为了方便访问登录表单,基本模板可以在导航栏中包含到它的链接:

app/templates/base.html: 导航栏中的登录链接

<div>
    Microblog:
    <a href="/index">Home</a>
    <a href="/login">Login</a>
</div>

此时,您可以运行应用程序并在web浏览器中查看表单. 程序运行后, 在地址栏驶入http://localhost:5000/, 然后点击在上方的导航栏内的 “Login” 链接来查看新的登录表单. 太酷了, 对吗?

Login Form

接收Form表单数据

如果你尝试按submit提交按钮, 浏览器将显示一个”Method Not Allowed”错误. 这是因为到目前为止,上一节中的login视图函数只完成了一半的工作. 它可以在web页面上显示表单,但是还没有处理用户提交的数据的逻辑. 这是Flask-WTF使工作变得非常容易的另一个方面. 下面是视图函数的更新版本,它接受并验证用户提交的数据:

app/routes.py: 接收登录凭证

from flask import render_template, flash, redirect

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for user {}, remember_me={}'.format(
            form.username.data, form.remember_me.data))
        return redirect('/index')
    return render_template('login.html', title='Sign In', form=form)

在这个版本中第一个新事物是路由装饰器中的methods参数. 这告诉Flask这个视图函数接受GETPOST请求, 覆盖默认的只接受GET请求. HTTP协议声明GET请求是那些返回给客户端信息的(在这里是web浏览器). 到目前为止应用程序内所有的请求都是这种类型. POST请求是当浏览器提交表单数据给服务器典型的请求 (事实上GET请求也可以用于此目的, 但是不推荐这样做). 之前浏览器现实的”Method Not Allowed” 错误, 是因为浏览器尝试发送一个POST请求,但是应用程序没有配置接收它. 通过提供methods参数, 你可以告诉Flask哪些请求方法被接受.

form.validate_on_submit()方法处理所有表单工作. 当浏览器发送GET请求来接收带有表单的web页面时, 这个方法将返回False, 在这种情况下,函数跳过if语句,直接在函数的最后一行呈现模板.

当用户按下submit按钮,浏览器发送POST请求时, form.validate_on_submit()将收集所有的数据, 运行所有附加到字段的验证器,如果一切正常,它将返回True, 指示数据是有效的,并且可以由应用程序处理。但是,如果至少有一个字段验证失败,那么函数将返回False, 这将导致表单被呈现回用户,就像在GET请求的情况下一样. 稍后,当验证失败时,我将添加一条错误消息.

form.validate_on_submit()返回True, 登录视图函数调用两个从Flask导入的新的函数. flash()函数是一个显示给用户消息的有用方法. 许多应用程序使用这种技术让用户知道某些操作是否成功. 在本例中,我将使用这种机制作为临时解决方案,因为我还没有所有必要的基础设施来真正地登录用户。目前我能做的最好的事情就是显示一条消息,确认应用程序收到了凭据.

登录视图函数中使用的第二个新函数是redirect(). 该函数指示客户机web浏览器自动导航到另一个页面,作为参数给出。这个视图函数使用它将用户重定向到应用程序的索引页.

当你调用flash()函数, Flask存储消息, 但是闪过的消息不会神奇地出现在web页面中. 应用程序的模板需要以适合站点布局的方式呈现这些闪烁的消息。我将把这些消息添加到基本模板中,以便所有模板都继承此功能。这是更新后的基本模板:

app/templates/base.html: 在基本模板中显示消息

<html>
    <head>
        {% if title %}
        <title>{{ title }} - microblog</title>
        {% else %}
        <title>microblog</title>
        {% endif %}
    </head>
    <body>
        <div>
            Microblog:
            <a href="/index">Home</a>
            <a href="/login">Login</a>
        </div>
        <hr>
        {% with messages = get_flashed_messages() %}
        {% if messages %}
        <ul>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
        </ul>
        {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </body>
</html>

这里我使用Here I’m using a with结构来指定get_flashed_messages()的调用结果给一个messages变量. get_flashed_messages()函数来自于Flask,它返回先前已在flash()中注册的所有消息的列表. 后面的条件检查messages是否具有某些内容,在这种情况下, <ul>元素中每个消息作为<li>列表项被渲染. 这种呈现样式看起来不太好,但是web应用程序样式化的主题将在稍后讨论.

这些闪过消息的一个有趣特性是,一旦通过get_flashed_messages函数请求它们一次,它们就会从消息列表中删除,因此它们只在调用flash()函数之后出现一次 .

现在是再次尝试应用程序并测试表单如何工作的好时机。确保您尝试提交用户名或密码字段为空的表单,以查看DataRequired如何停止提交过程.

改善字段验证

附加到表单字段的验证器防止将无效数据接受到应用程序中。应用程序处理无效表单输入的方法是重新显示表单,让用户进行必要的更正.

如果您尝试提交无效数据,我相信您已经注意到,虽然验证机制工作得很好,但是没有向用户指出表单有什么问题,用户只是简单地返回表单。下一个任务是通过在每个验证失败的字段旁边添加有意义的错误消息来改进用户体验.

实际上,表单验证器已经生成了这些描述性错误消息,因此缺少的只是模板中的一些额外逻辑来呈现它们.

这是登录模板,在用户名和密码字段中添加了字段验证消息:

app/templates/login.html: 登录表单模板中的验证错误

{% extends "base.html" %}

{% block content %}
    <h1>Sign In</h1>
    <form action="" method="post" novalidate>
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

我所做的惟一更改是在用户名和密码字段之后添加for循环,这些字段将验证器添加的错误消息呈现为红色. 一般来说,任何附加验证器的字段都将在form.<field_name>.errors下添加错误消息. 这将是一个列表,因为字段可以附加多个验证器,而且可能有多个验证器向用户显示错误消息.

如果您尝试使用空的用户名或密码提交表单,您现在将得到一条漂亮的红色错误消息.

Form validation

生成链接

现在登录表单已经相当完整, 但是在结束这个章节前我想谈一下在模板和重定向中包含链接的正确方法. 目前你已经看到了一些已定义的链接. 例如, 这是当前基础模板内的导航条:

    <div>
        Microblog:
        <a href="/index">Home</a>
        <a href="/login">Login</a>
    </div>

登录的视图函数通常通过redirect()函数定义一个链接 :

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect('/index')
    # ...

直接编写链接和源文件有一个问题, 那就是如果一天你决定整理你的链接, 你需要在你的整个应用程序查找和替换掉链接.

为了更好的控制这些链接, Flask提供一个名叫url_for()的函数, 它使用内置映射生成URL映射到视图函数. 例如, url_for('login') 返回/login,  url_for('index') 返回'/index. url_for()的参数是视图函数的名称.

你或许会问为什么使用函数名称代替URL更好. 事实是URL比视图函数的名称更容易改变. 次要原因即将在后面了解, 一些URL有动态组件, 所以通过手动生成这些URL需要连接多重元素, 这非常冗长且易错. url_for()也能够生成这些复杂的URL.

所以从现在开始, 每当我需要生成一个应用程序URL我会使用url_for(). 基础模板内的导航条将变为:

app/templates/base.html: 为链接使用url_for()函数

    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        <a href="{{ url_for('login') }}">Login</a>
    </div>

这是更新后的login()视图函数:

app/routes.py: 为链接使用url_for()函数

from flask import render_template, flash, redirect, url_for

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect(url_for('index'))
    # ...

章节 4: Database 数据库

Flask内的数据库

我相信你已经听说了, Flask自身不支持数据库. 这是Flask有意不发表意见的领域之一, 这样非常棒, 因为你可以自由的选择最适合你的应用程序的数据库,而不是被迫的适应数据库.

这有Python中对于数据库的非常棒的选择, 其中许多带有Flask扩展,可以更好地与应用程序集成. 数据库可以分为两大类, 关系型数据库, 和非关系型数据库. 后者通常被叫做NoSQL, 表示它们没有实现流行的关系查询语言SQL. 两组中都有很棒的数据库产品, 我的观点是,关系数据库更适合具有结构化数据(如用户列表)的应用程序, 博客文章等. 而NoSQL数据库更适合结构定义较少的数据. 与大多数其他应用程序一样,这个应用程序可以使用任何一种类型的数据库来实现,但是由于上述原因,我将使用关系数据库.

章节 3中我我向你展示了第一个Flask扩展. 在本章中,我将使用另外两个. 第一个是Flask-SQLAlchemy, 一个为流行的SQLAlchemy包提供Flask友好操作的包, 它是一个Object Relational Mapper或者说ORM(对象关系映射). ORMs 允许应用程序使用类等高级实体管理数据库, 对象和方法,而不是表和SQL. ORM的工作是将高级操作转换为数据库命令.

SQLAlchemy的优点是,它是一个ORM,不是只针对一个,而是针对许多关系数据库. SQLAlchemy支持数据库引擎的长列表, 包括流行的MySQLPostgreSQLSQLite. 这是非常强大的,因为你可以使用一个简单的SQLite数据库开发,不需要服务器,然后在有需要的时候在生产服务器上部署应用程序你可以选择一种更健壮的MySQL或PostgreSQL服务器,而无需更改应用程序.

为了在你的虚拟环境安装Flask-SQLAlchemy, 确保你已经激活了虚拟环境, 然后运行:

(venv) $ pip install flask-sqlalchemy

数据库迁移

我见过的大多数数据库教程都介绍了数据库的创建和使用,但是没有充分解决在应用程序需要更改或增长时更新现有数据库的问题. 这很困难,因为关系数据库是以结构化数据为中心的,所以当结构发生更改时,需要将数据库中已有的数据迁移到修改后的结构中.

在本章中,我要介绍的第二个扩展是Flask-Migrate, 它是真正需要你创建的. 这个扩展是一个Alembic的Flask包装, 一个SQLAlchemy的数据库迁移框架. 处理数据库迁移会增加一些启动数据库的工作,但是对于将来对数据库进行更改的健壮方法来说,这是一个很小的代价.

Flask-Migrate的安装过程与所见过的其他扩展类似:

(venv) $ pip install flask-migrate

Flask-SQLAlchemy配置

在开发期间,我将使用SQLite数据库。对于开发小型应用程序,SQLite数据库是最方便的选择,有时甚至不是非常小,因为每个数据库都存储在磁盘上的一个文件中,不需要运行MySQL和PostgreSQL这样的数据库服务器 .

我们有两个新的配置项添加到配置文件 :

config.py: Flask-SQLAlchemy配置

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    # ...
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

Flask-SQLAlchemy扩展从SQLALCHEMY_DATABASE_URI配置变量中获取应用程序数据库的位置. 还记得章节 3吧, 通常,从环境变量设置配置并在环境没有定义变量时提供回退值是一种很好的实践 . 在本例中我从DATABASE_URL环境变量获取数据库URL, 如果它没有定义, 我将在应用程序主目录配置一个名为app.db的数据库, 将它保存在basedir变量.

SQLALCHEMY_TRACK_MODIFICATIONS配置选项被设置为False,来禁用一个我不需要的Flask-SQLAlchemy的特性, 在每次数据库更改时向程序发送信号.

数据库将在应用程序中由数据库实例表示。数据库迁移引擎也将有一个实例. 这些对象需要在应用程序之后创建, 在app/__init__.py文件中:

app/__init__.py: Flask-SQLAlchemy 和 Flask-Migrate 初始化

from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)

from app import routes, models

我对初始化脚本做了三处修改. 首先, 我添加了一个代表数据库的db对象. 然后我添加了另一个表示迁移引擎的对象. 希望你已经了解了如何使用Flask扩展的模式. 大多数扩展像这两个扩展一样被初始化. 最后, 我在下面导入了一个名为models的模块. 这个模块将定义数据库的结构.

数据库模型

将存储在数据库中的数据将由一组类表示,通常称为数据库模型。SQLAlchemy中的ORM层将完成将从这些类创建的对象映射到适当数据库表中的行所需的转换 .

让我们从创建一个表示用户的模型开始. 使用 WWW SQL Designer 工具, 我制作了下图来表示我们想要在users表中使用的数据:

users table

id 字段通常在所有模型中,并用作主键. 数据库中的每个用户都将被分配一个惟一的id值,存储在这个字段中。在大多数情况下,主键是由数据库自动分配的,所以我只需要提供标识为主键的id字段.

usernameemail 和password_hash 字段被定义为字符串 (术语VARCHAR), 并指定它们的最大长度,以便数据库能够优化空间使用. username 和email 字段不言而喻, password_hash字段值得关注. 我希望确保正在构建的应用程序采用安全最佳实践,因此我不会将用户密码存储在数据库中. 存储密码的问题是,一旦数据库受到攻击,攻击者就可以访问密码,这对用户来说可能是毁灭性的。我不直接写密码,而是写密码散列, 它大大提高了安全性. 这将是另一章的主题,所以现在不要太担心.

现在我知道了用户表需要什么, 我可以在app/models.py将其转换为新的代码:

app/models.py: User数据库模型

from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))

    def __repr__(self):
        return '<User {}>'.format(self.username)    

上面创建的User类继承自db.Model, 一个来自于Flask-SQLAlchemy的基类. 这个类将几个字段定义为类变量. 字段被作为db.Column类的实例创建, 它以字段类型作为参数, 加上其他可选参数, 例如,允许我指出哪些字段是惟一的和索引的,这对于提高数据库搜索的效率非常重要.

__repr__方法告诉Python如何输出这个类的对象, 它在调试中有用. 可以像下面一样在Python解释器查看__repr__()方法的使用:

>>> from app.models import User
>>> u = User(username='susan', email='susan@example.com')
>>> u
<User susan>

创建迁移存储库

上一节中创建的模型类定义了这个应用程序的初始数据库结构(或模式)。但随着应用程序的不断增长,将需要更改结构,很可能添加新内容,但有时也需要修改或删除项。Alembic (Flask-Migrate使用的迁移框架)将以一种不需要重新创建数据库的方式更改这些模式 .

为了完成这个看似困难的任务,Alembic维护了一个迁移存储库,它是一个目录,在其中存储迁移脚本。每次对数据库模式进行更改时,都会向存储库添加迁移脚本,其中包含更改的详细信息。要将迁移应用到数据库,这些迁移脚本按创建它们的顺序执行.

Flask-Migrate通过flask命令模式公开它的命令. 你已经见到过flask run, 它是Flask原生的子命令. flask db是增加的Flask-Migrate管理所有数据库迁移的子命令. 所以,让我们运行flask db init命令为项目创建迁移存储库:

(venv) $ flask db init
  Creating directory /home/miguel/microblog/migrations ... done
  Creating directory /home/miguel/microblog/migrations/versions ... done
  Generating /home/miguel/microblog/migrations/alembic.ini ... done
  Generating /home/miguel/microblog/migrations/env.py ... done
  Generating /home/miguel/microblog/migrations/README ... done
  Generating /home/miguel/microblog/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '/home/miguel/microblog/migrations/alembic.ini' before proceeding.

记住flask命令依靠FLASK_APP 环境变量来了解Flask应用程序放置在哪. 在本程序, 你想设置FLASK_APP=microblog.py, 如第一章所述.

运行此命令后, 你将发现一个新的migrations 目录, 里面有一些文件和版本子目录. 从现在开始,所有这些文件都应该作为项目的一部分来处理,特别是应该添加到源代码控制中.

第一次数据库迁移

迁移存储库就绪后,就可以创建第一个数据库迁移,其中包括映射到用户数据库模型的User表. 这有两种方式创建数据迁移: 人工或自动. 为了自动生成迁移, Alembic将数据库模型定义的数据库模式与数据库中当前使用的实际数据库模式进行比较. 然后,它使用必要的更改填充迁移脚本,使数据库模式与应用程序模型匹配. 在这里, 因为没有之前的数据库, 自动迁移将添加完整的User模型到迁移脚本. flask db migrate子命令生成这些自动迁移:

(venv) $ flask db migrate -m "users table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
  Generating /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done

该命令的输出让您了解迁移中包含的Alembic。前两行是信息,通常可以忽略。然后,它说它找到了一个用户表和两个索引。然后它会告诉您迁移脚本是在哪里编写的. e517276bb1c2代码是为迁移自动生成的惟一代码(对您来说是不同的). 带有-m选项的注释是可选的,它向迁移添加了一个简短的描述性文本.

生成的迁移脚本现在是项目的一部分,需要合并到源代码控制中。如果您对脚本的外观感到好奇,欢迎您查看它. 你将发现它有两个函数,名叫upgrade() 和downgrade(). upgrade()函数应用迁移, downgrade() 函数移除它. 这允许Alembic使用降级路径将数据库迁移到历史上的任何位置,甚至迁移到更早的版本.

flask db migrate 命令不对数据库做任何更改,它只生成迁移脚本. 要将更改应用到数据库,必须使用flask db upgrade命令.

(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> e517276bb1c2, users table

因为这个程序使用的是SQLite, upgrade命令将检测到数据库不存在并创建它(您将注意到在该命令完成后添加了一个名为app.db的文件,即SQLite数据库). 在使用MySQL和PostgreSQL等数据库服务器时,必须在运行upgrade之前在数据库服务器中创建数据库 .

注意,默认情况下,Flask-SQLAlchemy使用数据库表的“snake case”命名约定. 对于上面的User模型, 数据库中对应的表将被命名为 user. 对于一个AddressAndPhone模型类, 表名将为address_and_phone. 如果您喜欢选择自己的表名,您可以向模型类添加一个名为__tablename__ 的属性,并将其设置为所需的字符串名称.

数据库升级和降级工作流

目前,应用程序还处于起步阶段,但是讨论未来的数据库迁移策略并没有什么坏处。假设您将应用程序部署在开发机器上,并将一个副本部署到在线使用的生产服务器上.

假设下一个应用程序版本必须对模型进行更改,例如需要添加一个新表。如果没有迁移,您将需要弄清楚如何更改数据库的模式,无论是在开发机器中,还是在服务器中,这可能需要做很多工作.

但是使用数据库迁移支持,在您修改应用程序中的模型之后,您将生成一个新的迁移脚本 (flask db migrate), 您可能会检查它,以确保自动生成做了正确的事情,然后将更改应用到开发数据库(flask db upgrade). 您将向源代码控制添加迁移脚本并提交它.

当您准备将应用程序的新版本发布到生产服务器时,您所需要做的就是获取应用程序的更新版本,其中包括新的迁移脚本,并运行 flask db upgrade. Alembic将检测到生产数据库没有更新到模式的最新版本,并运行在前一个版本之后创建的所有新的迁移脚本.

正如我前面提到的,您还有一个flask db downgrade命令,它撤消了最后一次迁移. 虽然在生产系统上不太可能需要这个选项,但是在开发过程中可能会发现它非常有用。您可能生成了一个迁移脚本并应用了它,但却发现您所做的更改并不完全是您所需要的。在这种情况下,您可以降级数据库,删除迁移脚本,然后生成一个新的脚本来替换它.

数据库的关系

关系数据库擅长存储数据项之间的关系。考虑一个用户写博客的情况. 用户将在users表中有一条记录,而文章将在posts表中有一条记录。记录谁写了一篇文章的最有效的方法是将这两个相关的记录链接起来.

一旦用户和帖子之间建立了链接,数据库就可以回答关于该链接的查询。最简单的一个是当你有一篇博客文章,并且需要知道是什么用户写的。更复杂的查询与此相反。如果您有一个用户,您可能想知道该用户所写的所有帖子。Flask-SQLAlchemy将帮助处理这两种类型的查询.

让我们扩展数据库来存储博客文章,以查看实际的关系。下面是新posts 表的模式:

posts table

posts表有一个必需的id字段, 文章body(主体)和timestamp(时间戳). 但是除了这些预期的字段之外,我还添加了一个user_id 字段,它将文章链接到作者. 你已经看到所有用户都有一个唯一的id主键. 将博客文章链接到作者的用户的方法是向用户的id添加引用,这正是user_id 字段的含义. 这个  user_id  字段称为外键。上面的数据库图将外键显示为它所引用的表的字段和 id 字段之间的链接。这种关系称为一对多关系,因为“一个”用户写“许多”帖子.

修改后的 app/models.py如下所示:

app/models.py: 文章数据库表和关系

from datetime import datetime
from app import db

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    password_hash = db.Column(db.String(128))
    posts = db.relationship('Post', backref='author', lazy='dynamic')

    def __repr__(self):
        return '<User {}>'.format(self.username)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.String(140))
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    def __repr__(self):
        return '<Post {}>'.format(self.body)

新的 Post 类将表示用户所写的博客文章. timestamp字段将被索引,如果您想按时间顺序检索帖子,这是非常有用的. 我还添加了一个default参数, 传递给datetime.utcnow函数. 当您将函数作为默认值传递时,SQLAlchemy将把字段设置为调用该函数的值(注意:utcnow后面我没有加(), 所以我传递的是函数, 而不是调用的结果). 通常,您希望在服务器应用程序中使用UTC日期和时间。这确保您使用统一的时间戳,而不管用户位于何处。这些时间戳将在显示时转换为用户的本地时间.

user_id 字段初始化为user.id的外键, 这意味着它从users表引用一个 id 值. 在这里user是数据库表模型的名称. 在某些情况下,例如在db.relationship()调用, 模型由模型类引用,模型类通常以大写字母开头, 而在其他情况下,比如这个db.ForeignKey() 声明,模型由它的数据库表名给出, SQLAlchemy自动使用小写字符,对于多字模型名,使用snake大小写.

User类有一个新的posts字段, 它是由db.relationship初始化的. 这不是一个实际的数据库字段,而是用户和帖子之间关系的高级视图,因此不在数据库图中. 对于一个一对多关系, db.relationship字段通常在“一”一侧定义,并用作访问“多”的方便方法. 例如, 如果我有一个用户存储在u, u.posts表达式将运行数据库查询返回该用户的所有posts文章. db.relationship的第一个参数是表示”多“的一侧的模型类. 如果稍后在模块中定义了模型,则可以将此参数作为类名的字符串提供. backref参数定义了一个字段的名称,该字段将被添加到指向“one”对象的“many”类的对象中. 这将添加一个post.author表达式,它将返回帖子的作者. lazy参数定义如何发出关系的数据库查询,这将在稍后讨论. 如果还不是太明白,请不要担心,我将在本文的最后展示一些示例.

因为我更新了应用程序模型, 需要生成一个新的数据库迁移:

(venv) $ flask db migrate -m "posts table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'post'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
  Generating /home/miguel/microblog/migrations/versions/780739b227a7_posts_table.py ... done

迁移需要应用到数据库:

(venv) $ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade e517276bb1c2 -> 780739b227a7, posts table

如果您将项目存储在源代码控制中,还请记住向其添加新的迁移脚本.

游戏时间

我已经让你经历了一个定义数据库的漫长过程,但是我还没有向你展示所有内容是如何工作的. 由于应用程序还没有任何数据库逻辑,让我们在Python解释器中使用数据库来熟悉它. 所以继续运行python。 在启动解释器之前,请确保激活了虚拟环境.

进入Python命令行, 导入数据库实例和模型:

>>> from app import db
>>> from app.models import User, Post

首先创建一个新的用户:

>>> u = User(username='john', email='john@example.com')
>>> db.session.add(u)
>>> db.session.commit()

对数据库的更改是在会话的上下文中完成的,可以将会话访问为db.session。 可以在一个会话中累积多个更改,一旦所有更改都已注册,就可以发出一个db.session.commit(), 它自动地编写所有更改. 如果在处理会话时出现错误,对db.session.rollback()的调用将中止会话并删除其中存储的任何更改. 需要记住的重要一点是,只有在调用db.session.commit()时才会将更改写入数据库. 会话保证数据库永远不会处于不一致的状态.

创建另一个用户:

>>> u = User(username='susan', email='susan@example.com')
>>> db.session.add(u)
>>> db.session.commit()

数据库可以返回查询所有用户的结果:

>>> users = User.query.all()
>>> users
[<User john>, <User susan>]
>>> for u in users:
...     print(u.id, u.username)
...
1 john
2 susan

所有模型都有一个query属性,该属性是运行数据库查询的入口点. 最基本的查询是返回该类的所有元素的查询,该查询的名称为all(). 注意id属性在用户添加时自动设置.

这里是另一种查询方法. 如果你知道用户的 id , 你可以像下面一样检索用户:

>>> u = User.query.get(1)
>>> u
<User john>

现在创建一个博客文章:

>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)
>>> db.session.add(p)
>>> db.session.commit()

我不需要设置timestamp字段的值,因为这个字段有默认值, 你可以在模型的定义中找到. user_id字段呢? 回想一下我在用户类中创建的posts属性中的db.relationship, 还有一个author属性. 我使用author虚拟字段为一篇文章分配作者,而不必处理用户id. SQLAlchemy在这方面非常出色,因为它提供了关系和外键的高级抽象.

为了完善会话, 来看几个其它的查询:

>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]

>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]

>>> # print post author and body for all posts 
>>> posts = Post.query.all()
>>> for p in posts:
...     print(p.id, p.author.username, p.body)
...
1 john my first post!

# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User susan>, <User john>]

可以从Flask-SQLAlchemy学习更多数据库查询.

为了完结此部分, 擦去上面创建的用户和文章, 清空数据库用于后面的章节:

>>> users = User.query.all()
>>> for u in users:
...     db.session.delete(u)
...
>>> posts = Post.query.all()
>>> for p in posts:
...     db.session.delete(p)
...
>>> db.session.commit()

Shell环境

滑稽的之前的部分你是怎么开始的吗, 在开启Python命令行后, 首先运行导入:

>>> from app import db
>>> from app.models import User, Post

当你处理你的应用程序时, 你通常需要在Python shell测试一些东西, 所以需要重复上面的步骤, 这是冗杂的. flask shell命令是flask命令里另一个非常有用的工具. shell命令是第二个实现Flask的核心命令, 在run后. 此命令的目的是在应用程序上下文中启动Python解释器. 这是什么意思?参见下面的示例 :

(venv) $ python
>>> app
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined
>>>

(venv) $ flask shell
>>> app
<Flask 'app'>

在常规的解释器会话, app符号在被明确的导入之前是不可使用的, 但使用 flask shell , 该命令预先导入应用程序实例. 关于flask shell的好处不是它预先导入了app,而是你可以配置一个 “shell context” ,这是一个预先导入的符号列表.

下面microblog.py 中的代码创建了一个预定义数据库实例和模型的shell上下文会话:

from app import app, db
from app.models import User, Post

@app.shell_context_processor
def make_shell_context():
    return {'db': db, 'User': User, 'Post': Post}

app.shell_context_processor装饰器将函数作为shell上下文函数注册. 当运行flask shell命令, 它将调用这个函数并在shell会话中注册它返回的项. 函数返回字典而不是列表的原因是,对于每个条目,您还必须提供一个名称,在shell中引用它,shell由字典键提供.

添加shell上下文处理器函数之后,就可以处理数据库实体,而不必导入它们:

(venv) $ flask shell
>>> db
<SQLAlchemy engine=sqlite:////Users/migu7781/Documents/dev/flask/microblog2/app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>

如果你在尝试上述方法, 并在访问 dbUserPost时得到NameError错误, 然后make_shell_context()函数没有在Flask中注册. 最可能的原因是没有设置环境变量 FLASK_APP=microblog.py . 如果那样, 回到章节一并查看怎么设置FLASK_APP环境变量. 如果在打开新的终端窗口时经常忘记设置此变量,可以考虑向项目中添加.flaskenv文件,如本章末尾所述.

章节 5: User Logins 用户登录

密码散列

章节4用户模型给定一个password_hash字段, 到目前为止还没有使用. 该字段的目的是保存用户密码的散列, 它将用于用户登录进程验证密码. 密码散列是一个复杂的主题,应该留给安全专家来解决,但是有几个易于使用的库以一种从应用程序调用的简单方式实现了所有逻辑.

实现密码散列的包之一是 Werkzeug, 您可能在安装Flask时在pip的输出中看到过引用, 因为它是它的核心依赖项之一. 由于它是一个依赖项,所以已经在虚拟环境中安装了Werkzeug. 下面的代码将会演示如何hash一个密码:

>>> from werkzeug.security import generate_password_hash
>>> hash = generate_password_hash('foobar')
>>> hash
'pbkdf2:sha256:50000$vT9fkZM8$04dfa35c6476acf7e788a1b5b3c35e217c78dc04539d295f011f01f18cd2175f'

在这个例子, 密码foobar通过一系列没有已知反向操作的加密操作转换为长编码字符串, 这意味着获得散列密码的人将无法使用散列密码获得原始密码. 作为一个额外的度量,如果您多次哈希相同的密码,您将得到不同的结果,因此这使得通过查看哈希值来确定两个用户是否拥有相同的密码是不可能的.

验证过程由Werkzeug的第二个函数完成,如下所示:

>>> from werkzeug.security import check_password_hash
>>> check_password_hash(hash, 'foobar')
True
>>> check_password_hash(hash, 'barfoo')
False

验证函数接受先前生成的密码散列和用户在登录时输入的密码. 如果用户提供的密码匹配则函数返回True, 否则返回False.

整个密码哈希逻辑可以实现为用户模型中的两种新方法:

app/models.py: 密码散列 和 验证

from werkzeug.security import generate_password_hash, check_password_hash

# ...

class User(db.Model):
    # ...

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

有了这两种方法,用户对象现在就可以进行安全密码验证,而不需要存储原始密码。下面是这些新方法的一个示例用法:

>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('mypassword')
>>> u.check_password('anotherpassword')
False
>>> u.check_password('mypassword')
True

介绍Flask-Login

在本章节我将介绍给你一个非常受欢迎的Flask扩展Flask-Login. 这个扩展管理用户登录状态, 例如,当应用程序“记住“用户已登录时, 用户可以登录到应用程序,然后导航到不同的页面. 它还提供了“记住我”功能,允许用户在关闭浏览器窗口后仍然登录。要为本章做好准备,可以从在虚拟环境中安装Flask-Login开始:

(venv) $ pip install flask-login

与其它扩展一样, Flask-Login需要在app/__init__.py 中的应用程序实例之后立即创建并初始化. 这就是这个扩展的初始化方式:

app/__init__.py: Flask-Login 初始化

# ...
from flask_login import LoginManager

app = Flask(__name__)
# ...
login = LoginManager(app)

# ...

为Flask-Login准备用户模型

The Flask-Login extension works with the application’s user model, and expects certain properties and methods to be implemented in it. This approach is nice, because as long as these required items are added to the model, Flask-Login does not have any other requirements, so for example, it can work with user models that are based on any database system.

以下是四项必修项目:

  • is_authenticated: 如果用户拥有有效凭证,则为True否则为False.
  • is_active: 如果用户的帐户是活动的,则为True否则为False.
  • is_anonymous: 对普通用户为False, 对特殊匿名用户为True.
  • get_id(): 一种方法,它以字符串的形式返回用户的唯一标识符(如果使用python2,则为unicode).

I can implement these four easily, but since the implementations are fairly generic, Flask-Login provides a mixin class called UserMixin that includes generic implementations that are appropriate for most user model classes. Here is how the mixin class is added to the model:

app/models.py: Flask-Login user mixin class

# ...
from flask_login import UserMixin

class User(UserMixin, db.Model):
    # ...

User Loader Function

Flask-Login keeps track of the logged in user by storing its unique identifier in Flask’s user session, a storage space assigned to each user who connects to the application. Each time the logged-in user navigates to a new page, Flask-Login retrieves the ID of the user from the session, and then loads that user into memory.

Because Flask-Login knows nothing about databases, it needs the application’s help in loading a user. For that reason, the extension expects that the application will configure a user loader function, that can be called to load a user given the ID. This function can be added in the app/models.py module:

app/models.py: Flask-Login user loader function

from app import login
# ...

@login.user_loader
def load_user(id):
    return User.query.get(int(id))

The user loader is registered with Flask-Login with the @login.user_loader decorator. The id that Flask-Login passes to the function as an argument is going to be a string, so databases that use numeric IDs need to convert the string to integer as you see above.

Logging Users In

Let’s revisit the login view function, which as you recall, implemented a fake login that just issued a flash() message. Now that the application has access to a user database and knows how to generate and verify password hashes, this view function can be completed.

app/routes.py: Login view function logic

# ...
from flask_login import current_user, login_user
from app.models import User

# ...

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        return redirect(url_for('index'))
    return render_template('login.html', title='Sign In', form=form)

The top two lines in the login() function deal with a weird situation. Imagine you have a user that is logged in, and the user navigates to the /login URL of your application. Clearly that is a mistake, so I want to not allow that. The current_user variable comes from Flask-Login and can be used at any time during the handling to obtain the user object that represents the client of the request. The value of this variable can be a user object from the database (which Flask-Login reads through the user loader callback I provided above), or a special anonymous user object if the user did not log in yet. Remember those properties that Flask-Login required in the user object? One of those was is_authenticated, which comes in handy to check if the user is logged in or not. When the user is already logged in, I just redirect to the index page.

In place of the flash() call that I used earlier, now I can log the user in for real. The first step is to load the user from the database. The username came with the form submission, so I can query the database with that to find the user. For this purpose I’m using the filter_by() method of the SQLAlchemy query object. The result of filter_by() is a query that only includes the objects that have a matching username. Since I know there is only going to be one or zero results, I complete the query by calling first(), which will return the user object if it exists, or None if it does not. In Chapter 4 you have seen that when you call the all() method in a query, the query executes and you get a list of all the results that match that query. The first() method is another commonly used way to execute a query, when you only need to have one result.

If I got a match for the username that was provided, I can next check if the password that also came with the form is valid. This is done by invoking the check_password() method I defined above. This will take the password hash stored with the user and determine if the password entered in the form matches the hash or not. So now I have two possible error conditions: the username can be invalid, or the password can be incorrect for the user. In either of those cases, I flash a message, and redirect back to the login prompt so that the user can try again.

If the username and password are both correct, then I call the login_user() function, which comes from Flask-Login. This function will register the user as logged in, so that means that any future pages the user navigates to will have the current_user variable set to that user.

To complete the login process, I just redirect the newly logged-in user to the index page.

Logging Users Out

I know I will also need to offer users the option to log out of the application. This can be done with Flask-Login’s logout_user() function. Here is the logout view function:

app/routes.py: Logout view function

# ...
from flask_login import logout_user

# ...

@app.route('/logout')
def logout():
    logout_user()
    return redirect(url_for('index'))

To expose this link to users, I can make the Login link in the navigation bar automatically switch to a Logout link after the user logs in. This can be done with a conditional in the base.html template:

app/templates/base.html: Conditional login and logout links

    <div>
        Microblog:
        <a href="{{ url_for('index') }}">Home</a>
        {% if current_user.is_anonymous %}
        <a href="{{ url_for('login') }}">Login</a>
        {% else %}
        <a href="{{ url_for('logout') }}">Logout</a>
        {% endif %}
    </div>

The is_anonymous property is one of the attributes that Flask-Login adds to user objects through the UserMixin class. The current_user.is_anonymous expression is going to be True only when the user is not logged in.

Requiring Users To Login

Flask-Login provides a very useful feature that forces users to log in before they can view certain pages of the application. If a user who is not logged in tries to view a protected page, Flask-Login will automatically redirect the user to the login form, and only redirect back to the page the user wanted to view after the login process is complete.

For this feature to be implemented, Flask-Login needs to know what is the view function that handles logins. This can be added in app/__init__.py:

# ...
login = LoginManager(app)
login.login_view = 'login'

The 'login' value above is the function (or endpoint) name for the login view. In other words, the name you would use in a url_for() call to get the URL.

The way Flask-Login protects a view function against anonymous users is with a decorator called @login_required. When you add this decorator to a view function below the @app.route decorators from Flask, the function becomes protected and will not allow access to users that are not authenticated. Here is how the decorator can be applied to the index view function of the application:

app/routes.py: @login\_required decorator

from flask_login import login_required

@app.route('/')
@app.route('/index')
@login_required
def index():
    # ...

What remains is to implement the redirect back from the successful login to the page the user wanted to access. When a user that is not logged in accesses a view function protected with the @login_required decorator, the decorator is going to redirect to the login page, but it is going to include some extra information in this redirect so that the application can then return to the first page. If the user navigates to /index, for example, the @login_required decorator will intercept the request and respond with a redirect to /login, but it will add a query string argument to this URL, making the complete redirect URL /login?next=/index. The next query string argument is set to the original URL, so the application can use that to redirect back after login.

Here is a snippet of code that shows how to read and process the next query string argument:

app/routes.py: Redirect to “next” page

from flask import request
from werkzeug.urls import url_parse

@app.route('/login', methods=['GET', 'POST'])
def login():
    # ...
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user is None or not user.check_password(form.password.data):
            flash('Invalid username or password')
            return redirect(url_for('login'))
        login_user(user, remember=form.remember_me.data)
        next_page = request.args.get('next')
        if not next_page or url_parse(next_page).netloc != '':
            next_page = url_for('index')
        return redirect(next_page)
    # ...

Right after the user is logged in by calling Flask-Login’s login_user() function, the value of the next query string argument is obtained. Flask provides a request variable that contains all the information that the client sent with the request. In particular, the request.args attribute exposes the contents of the query string in a friendly dictionary format. There are actually three possible cases that need to be considered to determine where to redirect after a successful login:

  • If the login URL does not have a next argument, then the user is redirected to the index page.
  • If the login URL includes a next argument that is set to a relative path (or in other words, a URL without the domain portion), then the user is redirected to that URL.
  • If the login URL includes a next argument that is set to a full URL that includes a domain name, then the user is redirected to the index page.

The first and second cases are self-explanatory. The third case is in place to make the application more secure. An attacker could insert a URL to a malicious site in the next argument, so the application only redirects when the URL is relative, which ensures that the redirect stays within the same site as the application. To determine if the URL is relative or absolute, I parse it with Werkzeug’s url_parse() function and then check if the netloc component is set or not.

Showing The Logged In User in Templates

Do you recall that way back in Chapter 2 I created a fake user to help me design the home page of the application before the user subsystem was in place? Well, the application has real users now, so I can now remove the fake user and start working with real users. Instead of the fake user I can use Flask-Login’s current_user in the template:

app/templates/index.html: Pass current user to template

{% extends "base.html" %}

{% block content %}
    <h1>Hi, {{ current_user.username }}!</h1>
    {% for post in posts %}
    <div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
    {% endfor %}
{% endblock %}

And I can remove the user template argument in the view function:

app/routes.py: Do not pass user to template anymore

@app.route('/')
@app.route('/index')
def index():
    # ...
    return render_template("index.html", title='Home Page', posts=posts)

This is a good time to test how the login and logout functionality works. Since there is still no user registration, the only way to add a user to the database is to do it via the Python shell, so run flask shell and enter the following commands to register a user:

>>> u = User(username='susan', email='susan@example.com')
>>> u.set_password('cat')
>>> db.session.add(u)
>>> db.session.commit()

If you start the application and try to access http://localhost:5000/ or http://localhost:5000/index, you will be immediately redirected to the login page, and after you log in using the credentials of the user that you added to your database, you will be returned to the original page, in which you will see a personalized greeting.

User Registration

The last piece of functionality that I’m going to build in this chapter is a registration form, so that users can register themselves through a web form. Let’s begin by creating the web form class in app/forms.py:

app/forms.py: User registration form

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User

# ...

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

There are a couple of interesting things in this new form related to validation. First, for the email field I’ve added a second validator after DataRequired, called Email. This is another stock validator that comes with WTForms that will ensure that what the user types in this field matches the structure of an email address.

Since this is a registration form, it is customary to ask the user to type the password two times to reduce the risk of a typo. For that reason I have password and password2 fields. The second password field uses yet another stock validator called EqualTo, which will make sure that its value is identical to the one for the first password field.

I have also added two methods to this class called validate_username() and validate_email(). When you add any methods that match the pattern validate_<field_name>, WTForms takes those as custom validators and invokes them in addition to the stock validators. In this case I want to make sure that the username and email address entered by the user are not already in the database, so these two methods issue database queries expecting there will be no results. In the event a result exists, a validation error is triggered by raising ValidationError. The message included as the argument in the exception will be the message that will be displayed next to the field for the user to see.

To display this form on a web page, I need to have an HTML template, which I’m going to store in file app/templates/register.html. This template is constructed similarly to the one for the login form:

app/templates/register.html: Registration template

{% extends "base.html" %}

{% block content %}
    <h1>Register</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.username.label }}<br>
            {{ form.username(size=32) }}<br>
            {% for error in form.username.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

The login form template needs a link that sends new users to the registration form, right below the form:

app/templates/login.html: Link to registration page

    <p>New User? <a href="{{ url_for('register') }}">Click to Register!</a></p>

And finally, I need to write the view function that is going to handle user registrations in app/routes.py:

app/routes.py: User registration view function

from app import db
from app.forms import RegistrationForm

# ...

@app.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(username=form.username.data, email=form.email.data)
        user.set_password(form.password.data)
        db.session.add(user)
        db.session.commit()
        flash('Congratulations, you are now a registered user!')
        return redirect(url_for('login'))
    return render_template('register.html', title='Register', form=form)

And this view function should also be mostly self-explanatory. I first make sure the user that invokes this route is not logged in. The form is handled in the same way as the one for logging in. The logic that is done inside the if validate_on_submit() conditional creates a new user with the username, email and password provided, writes it to the database, and then redirects to the login prompt so that the user can log in.

Registration Form

With these changes, users should be able to create accounts on this application, and log in and out. Make sure you try all the validation features I’ve added in the registration form to better understand how they work. I am going to revisit the user authentication subsystem in a future chapter to add additional functionality such as to allow the user to reset the password if forgotten. But for now, this is enough to continue building other areas of the application.

发表回复