Flask-Web-应用构建指南-全-

Flask Web 应用构建指南(全)

原文:zh.annas-archive.org/md5/5AC5010B2FEF93C4B37A69C597C8617D

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在我们“现在的世界”中,人们几乎无法开发新的应用程序而不将许多技术粘合在一起,无论是新趋势数据库,消息系统还是各种语言。 谈到 Web 开发,事情可能会变得稍微复杂,因为你不仅必须将许多技术混合在一起,而且它们还必须与访问它们的应用程序(也称为 Web 浏览器)良好地配合。 它们还应与您的部署服务器兼容,这本身又是另一回事!

在 Python 世界中,人们遵循 Python 之禅和 PEP8 等伟大准则,交付令人惊叹的桌面软件,我们可以利用各种库和框架来创建出色的 Web 应用程序,每个都有其自己的哲学。 例如,Django 是一个捆绑解决方案; 它为您做出了关于项目应该如何看起来,应该有什么,以及应该如何完成事情的选择。 Web2py 是另一个框架解决方案,甚至将 IDE 与其捆绑在一起。 这些都是很好的概念,但如果您想创建一些简单的东西,我建议您在其他地方做。 它们通常是很好的选择,但有时它们只是太多了(最新的 Django 版本似乎决定改变这一点; 让我们密切关注进一步的发展)。

Flask 定位自己不是像 Django 和 Web2py 那样的开箱即用的解决方案,而是一个最简解决方案,你只得到最基本的东西,然后选择其他所有的东西。 当你想要对应用程序进行细粒度控制时,当你想要精确选择你的组件时,或者当你的解决方案很简单时(不是简化的,好吗?)。

这本书是对 Web 世界中美丽代码和许多选择的情景的回应。 它试图解决与 Web 开发相关的主要问题,从安全性到内容交付,从会话管理到 REST 服务和 CRUD。 还涵盖了重要的现代概念,如过度工程,质量和开发过程,以便从第一天起获得更好的结果。 为了使学习过程顺利进行,主题都是在不着急的情况下呈现,并附有注释示例。 该书还旨在为读者提供有关如何预防代码常见问题的现实建议。

来学习如何创建出色的 Flask 应用程序,为您的项目和客户提供价值!

本书涵盖的内容

第一章《Flask in a Flask, I Mean, Book》向你介绍了 Flask,解释了它是什么,它不是什么,以及它在 Web 框架世界中的定位。

第二章《First App, How Hard Could it Be?》涵盖了通往 Flask 开发的第一步,包括环境设置,你自己的“Hello World”应用程序,以及模板如何进入这个方程式。 这是一个轻松的章节!

第三章《Man, Do I Like Templates!》介绍了面部标签和过滤器在 Jinja2 模板引擎中的进展,以及它如何与 Flask 集成。 从这里开始事情开始变得有点严肃!

第四章《Please Fill in This Form, Madam》讨论了如何处理表单(因为表单是 Web 开发生活中的一个事实),并使用 WTForms 以其全部荣耀来对待它们!

第五章《Where Do You Store Your Stuff?》介绍了关系型和非关系型数据库的概念,涵盖了如何处理这两种情况,以及何时处理。

第六章《But I Wanna REST Mom, Now!》是关于创建 REST 服务的一章(因为 REST 的热情必须得到满足),手动创建和使用令人惊叹的 Flask-Restless。

第七章,“如果没有经过测试,就不是游戏,兄弟!”,是我们以质量为中心的章节,您将学习通过适当的测试、TDD 和 BDD 方式提供质量!

第八章,“技巧和窍门或 Flask 魔法 101”,是一个密集的章节,涵盖了良好的实践、架构、蓝图、调试和会话管理。

第九章,“扩展,我是如何爱你”,涵盖了到目前为止尚未涉及的所有伟大的 Flask 扩展,这些扩展将帮助您实现现实世界对您的生产力要求。

第十章,“现在怎么办?”,结束了我们的开发之旅,涵盖了健康部署的所有基础知识,并指引您在 Flask 世界中迈出下一步。

您需要为本书做好准备

为了充分利用阅读体验,读者应该准备一台安装了 Ubuntu 14.x 或更高版本的机器,因为示例是为这种设置设计的,还需要对 Python 有基本的了解(如果您没有,请先参考learnxinyminutes.com/docs/python/),以及一个带有您喜欢的高亮显示的文本编辑器(LightTable,Sublime,Atom)。其他所需软件将在各章讨论中介绍。

这本书是为谁准备的

本书面向 Python 开发人员,无论是有一些或没有 Web 开发经验的人,都希望创建简约的 Web 应用程序。它专注于那些希望成为 Web 开发人员的人,因为所有基础知识都在一定程度上得到了涵盖,也专注于那些已经熟悉使用其他框架进行 Web 开发的人,无论是基于 Python 的框架,如 Django、Bottle 或 Pyramid,还是其他语言的框架。

同样重要的是,您要对用于构建网页的 Web 技术有基本的了解,比如 CSS、JavaScript 和 HTML。如果这不是您的背景,请查看 W3Schools 网站(w3schools.com/),因为它涵盖了使用这些技术的基础知识。此外,如果您熟悉 Linux 终端,整本书的学习将会更加轻松;如果不是这种情况,请尝试链接help.ubuntu.com/community/UsingTheTerminal

尽管如此,请放心,如果您对 Python 有基本的了解,您完全有能力理解示例和章节;在本书结束时,您将创建出表现良好且易于维护的令人惊叹的 Web 应用程序。

约定

在本书中,您将找到一些区分不同信息类型的文本样式。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“进入新项目文件夹并创建main.py文件”。

代码块设置如下:

# coding:utf-8
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

任何命令行输入或输出都以以下形式编写:

sudo pip install virtualenvwrapper

新术语重要单词以粗体显示。您在屏幕上看到的单词,比如菜单或对话框中的单词,会以这样的形式出现在文本中:“您有没有想象过在网站上填写表单并在末尾点击那个花哨的发送按钮时会发生什么?”。

注意

警告或重要说明会以这样的方式出现在框中。

提示

提示和技巧会以这样的方式出现。

第一章:Flask 中的 Flask,我的意思是,书

Flask 是什么?这是一个人类几千年来一直在思考的问题……嗯,实际上是自 2010 年 Armin Ronacher 首次承诺该项目以来。Flask 是一个 Web 框架,与大多数人习惯使用的方式非常不同。它对你的应用程序应该是什么样子或者你应该使用什么来使其可用并不那么自以为是。这个 BSD 许可的软件包就是这样!

Flask 及其特性简介

Flask 框架实际上是一个非常好的胶水,它将令人惊叹的 Werkzeug 和 Jinja2 框架粘合在一起,负责响应请求和呈现输出(HTML,也许)。在 MVC 架构中,也被称为模型-视图-控制器,Flask 涵盖了 C 和 V。但 M 在哪里?Flask 并没有为你提供集成的模型层,因为对于 Web 应用程序实际上并不需要。如果你确实需要使用数据库,只需从众多可用的数据库解决方案中选择一个并创建自己的模型层,这并不难,而且会让你很开心!微框架的概念,怀着良好的意图,专门为 Flask 而设计,就是要给你提供你所需要的最小(但也是最有用的)功能集,而且不会妨碍你。

框架中必须具备哪些特性?

  • 开发服务器和调试器(健全友好)

  • Unicode 支持(拉丁语言友好)

  • WSGI 兼容(uWsgi 友好)

  • 单元测试客户端(质量代码)

  • URL 路由(它让我感动得流泪,它太美了!)

  • 请求分发

  • 安全的 cookies

  • 会话

  • Jinja2 模板(标签、过滤器、宏等)

有了这些,你可以处理 Ajax 请求、浏览器请求和请求之间的用户会话;将 HTTP 请求路由到你的控制器;评估表单数据;响应 HTML 和 JSON 等等。

这很好,但 Flask 不是一个 MVC 框架吗?嗯,这是值得讨论的。如果一个 Web 框架不实现 MVC 反模式,比如在视图中处理请求或混合模型和控制器,它可能有助于实现 MVC,这在我看来是最好的,因为它不会强制你的应用程序结构。

注意

Flask 不是一个 MVC 框架,因为它没有实现模型层,尽管如果你希望创建自己的模型层,它不会限制你。

如果你需要一个简单的、单文件的 Web 应用程序,接收一个表单并返回一个答案,无论是 HTML 还是其他,Flask 都可以帮助你轻松实现。如果你需要一个多层、高深度模块化的 Facebook 克隆,Flask 也可以为你提供帮助。

那么,我们到目前为止学到了什么?

  • Flask 诞生于 2010 年

  • Flask 是一个基于 Jinja2 和 Werkzeug 的极简主义 Web 框架

  • Flask 不强制执行特定的项目架构

注意

请参考 Flask 许可证的详细信息flask.pocoo.org/docs/0.10/license/

现在,你可能想知道你的哪些好点子可以用 Flask 来实现。这就对了!我们一起来想想这个问题吧?

Flask 在数据库集成、表单库、管理界面或迁移工具方面没有捆绑功能。你可以通过扩展来实现这些功能,这些扩展很快就会讨论到,但它们都是外部的 Flask。如果你需要这些扩展,而且不想在项目开始时设置它们(或者没有时间),你可能更适合使用一个功能齐全的 MVC 一体化、低内聚、高耦合的框架,比如 Django。

现在,想象一下你需要建立一个网站,只有一个表单,比如cashcash.cc/的克隆,它接收一个表单并返回当前的货币交易价值;Flask 可以帮助你快速完成项目。

让我们再深入思考一下。如果你需要一组特定的库在你的项目中一起工作,而你又不希望 Web 框架妨碍你;这对于 Flask 来说是另一个非常好的场景,因为它给你提供了最基本的东西,让你自己组合其他你可能需要的一切。一些框架对它们自己的组件有如此高的耦合(读作依赖性),以至于如果你想使用特定的替代方案,你可能会遇到严重的问题。

例如,你可能想在项目中使用 NoSQL 数据库;然而,如果这样做,你的项目的一些组件可能会停止工作(例如:管理组件)。

基本上,如果你有时间可以抽出来,如果你正在做一些简单的事情,如果你想要实现自己的架构解决方案,或者如果你需要对项目中使用的组件进行细粒度控制,Flask 就是适合你的 Web 框架。

总结

现在,让我们谈谈令人敬畏的事情,在读完这本书后,你将能够处理 HTTP 和 Ajax 请求;创建具有数据库集成(SQL 和 NoSQL)和 REST 服务的完整功能的 Web 应用程序;使用 Flask 扩展(表单、缓存、日志、调试、认证、权限等);以及模块化和对应用程序进行单元和功能测试。

希望你喜欢这本书,并能用所学的知识做出很棒的东西

第二章:第一个应用程序,有多难?

在一个完整的章节中没有一行代码,你需要这个,对吧?在这一章中,我们将逐行解释我们的第一个应用程序;我们还将介绍如何设置我们的环境,开发时使用什么工具,以及如何在我们的应用程序中使用 HTML。

Hello World

当学习新技术时,人们通常会写一个 Hello World 应用程序,这个应用程序包含启动一个简单应用程序并显示文本"Hello World!"所需的最小可能代码。让我们使用 Flask 来做到这一点。

本书针对Python 2.x进行了优化,所以我建议你从现在开始使用这个版本。所有的示例和代码都针对这个 Python 版本,这也是大多数 Linux 发行版的默认版本。

先决条件和工具

首先,让我们确保我们的环境已经正确配置。在本课程中,我假设你使用的是类似 Debian 的 Linux 发行版,比如 Mint(www.linuxmint.com/)或 Ubuntu(ubuntu.com/)。所有的说明都将针对这些系统。

让我们从以下方式开始安装所需的 Debian 软件包:

sudo apt-get install python-dev python-pip

这将安装 Python 开发工具和编译 Python 包所需的库,以及 pip:一个方便的工具,你可以用它来从命令行安装 Python 包。继续吧!让我们安装我们的虚拟环境管理工具:

sudo pip install virtualenvwrapper
echo "source /usr/local/bin/virtualenvwrapper.sh" >> ~/.bashrc

解释一下我们刚刚做的事情:sudo告诉我们的操作系统,我们想要以管理员权限运行下一个命令,pip是默认的 Python 包管理工具,帮助我们安装virtualenvwrapper包。第二个命令语句添加了一个命令,将virtualenvwrapper.sh脚本与控制台一起加载,以便命令在你的 shell 内工作(顺便说一下,我们将使用它)。

设置虚拟环境

虚拟环境是 Python 将完整的包环境与其他环境隔离开来的方式。这意味着你可以轻松地管理依赖关系。想象一下,你想为一个项目定义最小必需的包;虚拟环境将非常适合让你测试和导出所需包的列表。我们稍后会讨论这个问题。现在,按下键盘上的Ctrl + Shift + T创建一个新的终端,并像这样创建我们的hello world环境:

mkvirtualenv hello
pip install flask

第一行创建了一个名为"hello"的环境。你也可以通过输入deactivate来停用你的虚拟环境,然后可以使用以下命令再次加载它:

workon hello  # substitute hello with the desired environment name if needed

第二行告诉 pip 在当前虚拟环境hello中安装 Flask 包。

理解"Hello World"应用程序

在设置好环境之后,我们应该使用什么来编写我们美丽的代码呢?编辑器还是集成开发环境?如果你的预算有限,可以尝试使用 Light Table 编辑器(lighttable.com/)。免费、快速、易于使用(Ctrl + Spacebar 可以访问所有可用选项),它还支持工作区!对于这个价钱来说,已经很难找到更好的了。如果你有 200 美元可以花(或者有免费许可证www.jetbrains.com/pycharm/buy/),那就花钱购买 PyCharm 集成开发环境吧,这几乎是最适合 Python Web 开发的最佳 IDE。现在让我们继续。

创建一个文件夹来保存你的项目文件(你不需要,但如果你这样做,人们会更喜欢你),如下所示:

mkdir hello_world

进入新的项目文件夹并创建main.py文件:

cd hello_world
touch main.py

main.py文件将包含整个"Hello World"应用程序。我们的main.py内容应该像这样:

# coding:utf-8
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接发送到您的电子邮件。

哇!那需要一些打字,对吧?不是吗?是的,我知道。那么,我们刚刚做了什么?

第一行说明我们的main.py文件应该使用utf-8编码。所有酷孩子都这样做,所以不要对您的非英语朋友不友好,并在所有 Python 文件中使用它(这样做可能有助于您避免在大型项目中出现一些讨厌的错误)。

在第二行和第三行,我们导入我们的 Flask 类并对其进行实例化。我们应用程序的名称是“app”。几乎所有与它相关的东西都与它有关:视图、蓝图、配置等等。参数__name__是必需的,并且用于告诉应用程序在哪里查找静态内容或模板等资源。

为了创建我们的“Hello World”,我们需要告诉我们的 Flask 实例在用户尝试访问我们的 Web 应用程序(使用浏览器或其他方式)时如何响应。为此,Flask 有路由。

路由是 Flask 读取请求头并决定哪个视图应该响应该请求的方式。它通过分析请求的 URL 的路径部分,并找到注册了该路径的路由来实现这一点。

hello world示例中,在第 5 行,我们使用路由装饰器将hello函数注册到"/"路径。每当应用程序接收到路径为"/"的请求时,hello都会响应该请求。以下代码片段显示了如何检查 URL 的路径部分:

from urlparse import urlparse
parsed = urlparse("https://www.google.com/")
assert parsed.path == "/"

您还可以将多个路由映射到同一个函数,如下所示:

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

在这种情况下,"/""/index"路径都将映射到hello

在第 6 和第 7 行,我们有一个将响应请求的函数。请注意,它不接收任何参数并且以熟悉的字符串作出响应。它不接收任何参数,因为请求数据(如提交的表单)是通过一个名为request的线程安全变量访问的,我们将在接下来的章节中更多地了解它。

关于响应,Flask 可以以多种格式响应请求。在我们的示例中,我们以纯字符串作出响应,但我们也可以以 JSON 或 HTML 字符串作出响应。

第 9 和第 10 行很简单。它们检查main.py是作为脚本还是作为模块被调用。如果是作为脚本,它将运行与 Flask 捆绑在一起的内置开发服务器。让我们试试看:

python main.py

您的终端控制台将输出类似以下内容:

Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

只需在浏览器中打开http://127.0.0.1:5000/,即可查看您的应用程序运行情况。

main.py作为脚本运行通常是一个非常简单和方便的设置。通常,您可以使用 Flask-Script 来处理为您调用开发服务器和其他设置。

如果您将main.py作为模块使用,只需按以下方式导入它:

from main import what_I_want

通常,您会在测试代码中执行类似以下操作来导入应用工厂函数。

这基本上就是关于我们的“Hello World”应用程序的所有内容。我们的世界应用程序缺少的一件事是乐趣因素。所以让我们添加一些;让我们让您的应用程序有趣!也许一些 HTML、CSS 和 JavaScript 可以在这里起作用。让我们试试看!

提供 HTML 页面

首先,要使我们的hello函数以 HTML 响应,我们只需将其更改为以下内容:

def hello():
    return "<html><head><title>Hi there!</title></head><body>Hello World!</body></html>", 200

在上面的示例中,hello返回一个 HTML 格式的字符串和一个数字。字符串将默认解析为 HTML,而200是一个可选的 HTTP 代码,表示成功的响应。默认情况下返回200

如果您使用F5刷新浏览器,您会注意到没有任何变化。这就是为什么当源代码更改时,Flask 开发服务器不会重新加载。只有在调试模式下运行应用程序时才会发生这种情况。所以让我们这样做:

app = Flask(__name__)
app.debug=True

现在去你的应用程序正在运行的终端,输入Ctrl + C然后重新启动服务器。你会注意到除了你的服务器正在运行的 URL 之外有一个新的输出——关于“stat”的内容。这表示你的服务器将在源代码修改时重新加载代码。这很好,但你注意到我们刚刚犯下的罪行了吗:在处理响应的函数内部定义我们的模板?小心,MVC 之神可能在看着你。让我们把我们定义视图的地方和定义控制器的地方分开。创建一个名为 templates 的文件夹,并在其中创建一个名为index.html的文件。index.html文件的内容应该像这样:

<html>
<head><title>Hi there!</title></head>
<body>Hello World!</body>
</html>

现在改变你的代码像这样:

from flask import Flask, render_response
@app.route("/")
def hello():
    return render_template("index.html")

你看到我们做了什么了吗?render_response能够从templates/文件夹(Flask 的默认文件夹)中加载模板,并且你可以通过返回输出来渲染它。

现在让我们添加一些 JavaScript 和 CSS 样式。默认情况下,Flask 内置的开发服务器会提供project文件夹中名为static的子文件夹中的所有文件。让我们创建我们自己的文件夹并向其中添加一些文件。你的项目树应该是这样的:

project/
-main.py
-templates/
--index.html
-static/
--js
---jquery.min.js
---foundation.min.js
---modernizr.js
--css
---styles.css
---foundation.min.css

注意我从foundation.zurb框架中添加了文件,这是一个在foundation.zurb.com/上可用的不错的 CSS 框架。我建议你也这样做,以便拥有一个现代、漂亮的网站。你模板中的静态文件路径应该是这样的:

<script src='/static/js/modernizr.js'></script>

在真实文件路径之前的/static文件夹是 Flask 默认提供的路由,只在调试模式下起作用。在生产环境中,你将需要 HTTP 服务器为你提供静态文件。查看本章附带的代码以获取完整示例。

尝试用一些漂亮的 CSS 样式来改进“hello world”示例!

总结

建立开发环境是一项非常重要的任务,我们刚刚完成了这个任务!创建一个“Hello World”应用程序是向某人介绍新技术的好方法。我们也做到了。最后,我们学会了如何提供 HTML 页面和静态文件,这基本上是大多数 Web 应用程序所做的。你在本章中掌握了所有这些技能,我希望这个过程既简单又充实!

在下一章中,我们将通过更加冒险的模板来为我们的挑战增添一些调味。我们将学习如何使用 Jinja2 组件来创建强大的模板,从而让我们在输入更少的情况下做更多的事情。到时见!

第三章:天哪,我喜欢模板!

如前所述,Flask 为您提供了 MVC 中的 VC。在本章中,我们将讨论 Jinja2 是什么,以及 Flask 如何使用 Jinja2 来实现视图层并让您感到敬畏。做好准备!

Jinja2 是什么,它如何与 Flask 耦合在一起?

Jinja2 是一个库,可以在jinja.pocoo.org/找到;您可以使用它来生成带有捆绑逻辑的格式化文本。与 Python 格式函数不同,Python 格式函数只允许您用变量内容替换标记,您可以在模板字符串中使用控制结构(例如for循环),并使用 Jinja2 进行解析。让我们考虑这个例子:

from jinja2 import Template
x = """
<p>Uncle Scrooge nephews</p>
<ul>
{% for i in my_list %}
<li>{{ i }}</li>
{% endfor %}
</ul>
"""
template = Template(x)
# output is an unicode string
print template.render(my_list=['Huey', 'Dewey', 'Louie'])

在上面的代码中,我们有一个非常简单的例子,其中我们创建了一个模板字符串,其中包含一个for循环控制结构(简称“for 标签”),该结构遍历名为my_list的列表变量,并使用大括号{{ }}符号打印“li HTML 标签”中的元素。

请注意,您可以在模板实例中调用render多次,并使用不同的键值参数,也称为模板上下文。上下文变量可以有任何有效的 Python 变量名——也就是说,任何符合正则表达式[a-zA-Z_][a-zA-Z0-9_]格式的内容。

提示

有关 Python 正则表达式(Regex简称)的完整概述,请访问docs.python.org/2/library/re.html。还可以查看这个用于正则表达式测试的在线工具pythex.org/

一个更复杂的例子将使用环境类实例,这是一个中央、可配置、可扩展的类,可以以更有组织的方式加载模板。

您明白我们要说什么了吗?这是 Jinja2 和 Flask 背后的基本原理:它为您准备了一个环境,具有一些响应式默认设置,并让您的轮子转起来。

您可以用 Jinja2 做什么?

Jinja2 非常灵活。您可以将其与模板文件或字符串一起使用;您可以使用它来创建格式化文本,例如 HTML、XML、Markdown 和电子邮件内容;您可以组合模板、重用模板和扩展模板;甚至可以使用扩展。可能性是无穷无尽的,并且结合了良好的调试功能、自动转义和完整的 Unicode 支持。

注意

自动转义是 Jinja2 的一种配置,其中模板中打印的所有内容都被解释为纯文本,除非另有明确要求。想象一个变量x的值设置为<b>b</b>。如果启用了自动转义,模板中的{{ x }}将打印给定的字符串。如果关闭了自动转义,这是 Jinja2 的默认设置(Flask 的默认设置是开启的),则生成的文本将是b

在介绍 Jinja2 允许我们进行编码之前,让我们先了解一些概念。

首先,我们有前面提到的大括号。双大括号是一个分隔符,允许您从提供的上下文中评估变量或函数,并将其打印到模板中:

from jinja2 import Template
# create the template
t = Template("{{ variable }}")
# – Built-in Types –
t.render(variable='hello you')
>> u"hello you"
t.render(variable=100)
>> u"100"
# you can evaluate custom classes instances
class A(object):
  def __str__(self):
    return "__str__"
  def __unicode__(self):
    return u"__unicode__"
  def __repr__(self):
    return u"__repr__"
# – Custom Objects Evaluation –
# __unicode__ has the highest precedence in evaluation
# followed by __str__ and __repr__
t.render(variable=A())
>> u"__unicode__"

在上面的例子中,我们看到如何使用大括号来评估模板中的变量。首先,我们评估一个字符串,然后是一个整数。两者都会产生 Unicode 字符串。如果我们评估我们自己的类,我们必须确保定义了__unicode__方法,因为在评估过程中会调用它。如果没有定义__unicode__方法,则评估将退回到__str____repr__,依次进行。这很简单。此外,如果我们想评估一个函数怎么办?好吧,只需调用它:

from jinja2 import Template
# create the template
t = Template("{{ fnc() }}")
t.render(fnc=lambda: 10)
>> u"10"
# evaluating a function with argument
t = Template("{{ fnc(x) }}")
t.render(fnc=lambda v: v, x='20')
>> u"20"
t = Template("{{ fnc(v=30) }}")
t.render(fnc=lambda v: v)
>> u"30"

要在模板中输出函数的结果,只需像调用任何常规 Python 函数一样调用该函数。函数返回值将被正常评估。如果您熟悉 Django,您可能会注意到这里有一点不同。在 Django 中,您不需要使用括号来调用函数,甚至不需要向其传递参数。在 Flask 中,如果要对函数返回值进行评估,则始终需要使用括号。

以下两个示例展示了 Jinja2 和 Django 在模板中函数调用之间的区别:

{# flask syntax #}
{{ some_function() }}

{# django syntax #}
{{ some_function }}

您还可以评估 Python 数学运算。看一下:

from jinja2 import Template
# no context provided / needed
Template("{{ 3 + 3 }}").render()
>> u"6"
Template("{{ 3 - 3 }}").render()
>> u"0"
Template("{{ 3 * 3 }}").render()
>> u"9"
Template("{{ 3 / 3 }}").render()
>> u"1"

其他数学运算符也可以使用。您可以使用花括号分隔符来访问和评估列表和字典:

from jinja2 import Template
Template("{{ my_list[0] }}").render(my_list=[1, 2, 3])
>> u'1'
Template("{{ my_list['foo'] }}").render(my_list={'foo': 'bar'})
>> u'bar'
# and here's some magic
Template("{{ my_list.foo }}").render(my_list={'foo': 'bar'})
>> u'bar'

要访问列表或字典值,只需使用普通的 Python 表示法。对于字典,您还可以使用变量访问表示法访问键值,这非常方便。

除了花括号分隔符,Jinja2 还有花括号/百分比分隔符,它使用{% stmt %}的表示法,用于执行语句,这可能是控制语句,也可能不是。它的使用取决于语句,其中控制语句具有以下表示法:

{% stmt %}
{% endstmt %}

第一个标签具有语句名称,而第二个是闭合标签,其名称在开头附加了end。您必须意识到非控制语句可能没有闭合标签。让我们看一些例子:

{% block content %}
{% for i in items %}
{{ i }} - {{ i.price }}
{% endfor %}
{% endblock %}

前面的例子比我们之前看到的要复杂一些。它在块语句中使用了控制语句for循环(您可以在另一个语句中有一个语句),这不是控制语句,因为它不控制模板中的执行流程。在for循环中,您可以看到i变量与关联的价格(在其他地方定义)一起打印出来。

您应该知道的最后一个分隔符是{# comments go here #}。这是一个多行分隔符,用于声明注释。让我们看两个具有相同结果的例子:

{# first example #}
{#
second example
#}

两种注释分隔符都隐藏了{##}之间的内容。可以看到,这个分隔符适用于单行注释和多行注释,非常方便。

控制结构

Jinja2 中默认定义了一组不错的内置控制结构。让我们从if语句开始学习它。

{% if true %}Too easy{% endif %}
{% if true == true == True %}True and true are the same{% endif %}
{% if false == false == False %}False and false also are the same{% endif %}
{% if none == none == None %}There's also a lowercase None{% endif %}
{% if 1 >= 1 %}Compare objects like in plain python{% endif %}
{% if 1 == 2 %}This won't be printed{% else %}This will{% endif %}
{% if "apples" != "oranges" %}All comparison operators work = ]{% endif %}
{% if something %}elif is also supported{% elif something_else %}^_^{% endif %}

if控制语句很美妙!它的行为就像python if语句一样。如前面的代码所示,您可以使用它以非常简单的方式比较对象。"else"和"elif"也得到了充分支持。

您可能还注意到了truefalse,非大写,与普通的 Python 布尔值TrueFalse一起使用。为了避免混淆的设计决策,所有 Jinja2 模板都有TrueFalseNone的小写别名。顺便说一句,小写语法是首选的方式。

如果需要的话,您应该避免这种情况,可以将比较组合在一起以改变优先级评估。请参阅以下示例:

{% if  5 < 10 < 15 %}true{%else%}false{% endif %}
{% if  (5 < 10) < 15 %}true{%else%}false{% endif %}
{% if  5 < (10 < 15) %}true{%else%}false{% endif %}

前面示例的预期输出是truetruefalse。前两行非常直接。在第三行中,首先,(10<15)被评估为True,它是int的子类,其中True == 1。然后评估5 < True,这显然是假的。

for语句非常重要。几乎无法想象一个严肃的 Web 应用程序不必在某个时候显示某种列表。for语句可以迭代任何可迭代实例,并且具有非常简单的、类似 Python 的语法:

{% for item in my_list %}
{{ item }}{# print evaluate item #}
{% endfor %}
{# or #}
{% for key, value in my_dictionary.items() %}
{{ key }}: {{ value }}
{% endfor %}

在第一个语句中,我们有一个开放标签,指示我们将遍历my_list项,每个项将被名称item引用。名称item仅在for循环上下文中可用。

在第二个语句中,我们对形成my_dictionary的键值元组进行迭代,这应该是一个字典(如果变量名不够具有启发性的话)。相当简单,对吧?for循环也为您准备了一些技巧。

在构建 HTML 列表时,通常需要以交替颜色标记每个列表项,以改善可读性,或者使用一些特殊标记标记第一个和/或最后一个项目。这些行为可以通过在 Jinja2 for 循环中访问块上下文中可用的循环变量来实现。让我们看一些例子:

{% for i in ['a', 'b', 'c', 'd'] %}
{% if loop.first %}This is the first iteration{% endif %}
{% if loop.last %}This is the last iteration{% endif %}
{{ loop.cycle('red', 'blue') }}{# print red or blue alternating #}
{{ loop.index }} - {{ loop.index0 }} {# 1 indexed index – 0 indexed index #}
{# reverse 1 indexed index – reverse 0 indexed index #}
{{ loop.revindex }} - {{ loop.revindex0 }} 
{% endfor %}

for循环语句,就像 Python 一样,也允许使用else,但意义略有不同。在 Python 中,当您在for中使用else时,只有在没有通过break命令到达else块时才会执行else块,就像这样:

for i in [1, 2, 3]:
  pass
else:
  print "this will be printed"
for i in [1, 2, 3]:
  if i == 3:
    break
else:
  print "this will never not be printed"

如前面的代码片段所示,else块只有在for循环中从未被break命令中断执行时才会执行。使用 Jinja2 时,当for可迭代对象为空时,将执行else块。例如:

{% for i in [] %}
{{ i }}
{% else %}I'll be printed{% endfor %}
{% for i in ['a'] %}
{{ i }}
{% else %}I won't{% endfor %}

由于我们正在讨论循环和中断,有两件重要的事情要知道:Jinja2 的for循环不支持breakcontinue。相反,为了实现预期的行为,您应该使用循环过滤,如下所示:

{% for i in [1, 2, 3, 4, 5] if i > 2 %}
value: {{ i }}; loop.index: {{ loop.index }}
{%- endfor %}

在第一个标签中,您会看到一个普通的for循环和一个if条件。您应该将该条件视为一个真正的列表过滤器,因为索引本身只是在每次迭代中计数。运行前面的示例,输出将如下所示:

value:3; index: 1
value:4; index: 2
value:5; index: 3

看看前面示例中的最后一个观察——在第二个标签中,您看到{%-中的破折号吗?它告诉渲染器在每次迭代之前不应该有空的新行。尝试我们之前的示例,不带破折号,并比较结果以查看有何变化。

现在我们将看看用于从不同文件构建模板的三个非常重要的语句:blockextendsinclude

blockextends总是一起使用。第一个用于定义模板中的“可覆盖”块,而第二个定义了具有块的当前模板的父模板。让我们看一个例子:

# coding:utf-8
with open('parent.txt', 'w') as file:
    file.write("""
{% block template %}parent.txt{% endblock %}
===========
I am a powerful psychic and will tell you your past

{#- "past" is the block identifier #}
{% block past %}
You had pimples by the age of 12.
{%- endblock %}

Tremble before my power!!!""".strip())

with open('child.txt', 'w') as file:
    file.write("""
{% extends "parent.txt" %}

{# overwriting the block called template from parent.txt #}
{% block template %}child.txt{% endblock %}

{#- overwriting the block called past from parent.txt #}
{% block past %}
You've bought an ebook recently.
{%- endblock %}""".strip())
with open('other.txt', 'w') as file:
	file.write("""
{% extends "child.txt" %}
{% block template %}other.txt{% endblock %}""".strip())

from jinja2 import Environment, FileSystemLoader

env = Environment()
# tell the environment how to load templates
env.loader = FileSystemLoader('.')
# look up our template
tmpl = env.get_template('parent.txt')
# render it to default output
print tmpl.render()
print ""
# loads child.html and its parent
tmpl = env.get_template('child.txt')
print tmpl.render()
# loads other.html and its parent
env.get_template('other.txt').render()

您是否看到了child.txtparent.txt之间的继承?parent.txt是一个简单的模板,有两个名为templatepastblock语句。当您直接呈现parent.txt时,它的块会“原样”打印,因为它们没有被覆盖。在child.txt中,我们扩展parent.txt模板并覆盖所有其块。通过这样做,我们可以在模板的特定部分中具有不同的信息,而无需重写整个内容。

例如,使用other.txt,我们扩展child.txt模板并仅覆盖命名为 block 的模板。您可以从直接父模板或任何父模板覆盖块。

如果您正在定义一个index.txt页面,您可以在其中有默认块,需要时进行覆盖,从而节省大量输入。

解释最后一个示例,就 Python 而言,非常简单。首先,我们创建了一个 Jinja2 环境(我们之前谈到过这个),并告诉它如何加载我们的模板,然后直接加载所需的模板。我们不必费心告诉环境如何找到父模板,也不必预加载它们。

include语句可能是迄今为止最简单的语句。它允许您以非常简单的方式在另一个模板中呈现模板。让我们看一个例子:

with open('base.txt', 'w') as file:
  file.write("""
{{ myvar }}
You wanna hear a dirty joke?
{% include 'joke.txt' %}
""".strip())
with open('joke.txt', 'w') as file:
  file.write("""
A boy fell in a mud puddle. {{ myvar }} 
""".strip())

from jinja2 import Environment, FileSystemLoader

env = Environment()
# tell the environment how to load templates
env.loader = FileSystemLoader('.')
print env.get_template('base.txt').render(myvar='Ha ha!')

在前面的示例中,我们在base.txt中呈现joke.txt模板。由于joke.txtbase.txt中呈现,它也可以完全访问base.txt上下文,因此myvar会正常打印。

最后,我们有set语句。它允许您在模板上下文中定义变量。它的使用非常简单:

{% set x = 10 %}
{{ x }}
{% set x, y, z = 10, 5+5, "home" %}
{{ x }} - {{ y }} - {{ z }}

在前面的示例中,如果x是通过复杂计算或数据库查询给出的,如果要在模板中重复使用它,将其缓存在一个变量中会更有意义。如示例中所示,您还可以一次为多个变量分配一个值。

宏是您在 Jinja2 模板中最接近编码的地方。宏的定义和使用类似于普通的 Python 函数,因此非常容易。让我们尝试一个例子:

with open('formfield.html', 'w') as file:
  file.write('''
{% macro input(name, value='', label='') %}
{% if label %}
<label for='{{ name }}'>{{ label }}</label>
{% endif %}
<input id='{{ name }}' name='{{ name }}' value='{{ value }}'></input>
{% endmacro %}'''.strip())
with open('index.html', 'w') as file:
  file.write('''
{% from 'formfield.html' import input %}
<form method='get' action='.'>
{{ input('name', label='Name:') }}
<input type='submit' value='Send'></input>
</form>
'''.strip())

from jinja2 import Environment, FileSystemLoader

env = Environment()
env.loader = FileSystemLoader('.')
print env.get_template('index.html').render()

在前面的例子中,我们创建了一个宏,接受一个name参数和两个可选参数:valuelabel。在macro块内,我们定义了应该输出的内容。请注意,我们可以在宏中使用其他语句,就像在模板中一样。在index.html中,我们从formfield.html中导入输入宏,就好像formfield是一个模块,输入是一个使用import语句的 Python 函数。如果需要,我们甚至可以像这样重命名我们的输入宏:

{% from 'formfield.html' import input as field_input %}

您还可以将formfield作为模块导入并按以下方式使用它:

{% import 'formfield.html' as formfield %}

在使用宏时,有一种特殊情况,您希望允许任何命名参数传递到宏中,就像在 Python 函数中一样(例如,**kwargs)。使用 Jinja2 宏,默认情况下,这些值在kwargs字典中可用,不需要在宏签名中显式定义。例如:

# coding:utf-8
with open('formfield.html', 'w') as file:
    file.write('''
{% macro input(name) -%}
<input id='{{ name }}' name='{{ name }}' {% for k,v in kwargs.items() -%}{{ k }}='{{ v }}' {% endfor %}></input>
{%- endmacro %}
'''.strip())with open('index.html', 'w') as file:
    file.write('''
{% from 'formfield.html' import input %}
{# use method='post' whenever sending sensitive data over HTTP #}
<form method='post' action='.'>
{{ input('name', type='text') }}
{{ input('passwd', type='password') }}
<input type='submit' value='Send'></input>
</form>
'''.strip())

from jinja2 import Environment, FileSystemLoader

env = Environment()
env.loader = FileSystemLoader('.')
print env.get_template('index.html').render()

如您所见,即使您没有在宏签名中定义kwargs参数,kwargs也是可用的。

宏在纯模板上具有一些明显的优势,您可以通过include语句注意到:

  • 使用宏时,您不必担心模板中的变量名称

  • 您可以通过宏签名定义宏块的确切所需上下文

  • 您可以在模板中定义一个宏库,并仅导入所需的内容

Web 应用程序中常用的宏包括用于呈现分页的宏,用于呈现字段的宏,以及用于呈现表单的宏。您可能还有其他用例,但这些是相当常见的用例。

提示

关于我们之前的例子,使用 HTTPS(也称为安全 HTTP)发送敏感信息,如密码,通过互联网是一个良好的做法。要小心!

扩展

扩展是 Jinja2 允许您扩展其词汇的方式。扩展默认情况下未启用,因此只有在需要时才能启用扩展,并且可以在不太麻烦的情况下开始使用它:

env = Environment(extensions=['jinja2.ext.do', 'jinja2.ext.with_'])

在前面的代码中,我们有一个示例,其中您创建了一个启用了两个扩展的环境:dowith。这些是我们将在本章中学习的扩展。

正如其名称所示,do扩展允许您“做一些事情”。在do标记内,您可以执行 Python 表达式,并完全访问模板上下文。Flask-Empty 是一个流行的 Flask 样板,可在github.com/italomaia/flask-empty上找到,它使用do扩展来更新其宏之一中的字典。让我们看看我们如何做到这一点:

{% set x = {1:'home', '2':'boat'} %}
{% do x.update({3: 'bar'}) %}
{%- for key,value in x.items() %}
{{ key }} - {{ value }}
{%- endfor %}

在前面的例子中,我们使用一个字典创建了x变量,然后用{3: 'bar'}更新了它。通常情况下,您不需要使用do扩展,但是当您需要时,可以节省大量编码。

with扩展也非常简单。每当您需要创建块作用域变量时,都可以使用它。想象一下,您有一个需要在变量中缓存一小段时间的值;这将是一个很好的用例。让我们看一个例子:

{% with age = user.get_age() %}
My age: {{ age }}
{% endwith %}
My age: {{ age }}{# no value here #}

如示例所示,age仅存在于with块内。此外,在with块内设置的变量将仅在其中存在。例如:

{% with %}
{% set count = query.count() %}
Current Stock: {{ count }}
Diff: {{ prev_count - count }}
{% endwith %}
{{ count }} {# empty value #}

过滤器

过滤器是 Jinja2 的一个奇妙之处!这个工具允许您在将常量或变量打印到模板之前对其进行处理。目标是在模板中严格实现您想要的格式。

要使用过滤器,只需使用管道运算符调用它,就像这样:

{% set name = 'junior' %}
{{ name|capitalize }} {# output is Junior #}

它的名称被传递给capitalize过滤器进行处理,并返回大写的值。要将参数传递给过滤器,只需像调用函数一样调用它,就像这样:

{{ ['Adam', 'West']|join(' ') }} {# output is Adam West #}

join过滤器将连接传递的可迭代值,将提供的参数放在它们之间。

Jinja2 默认提供了大量可用的过滤器。这意味着我们无法在这里覆盖它们所有,但我们当然可以覆盖一些。capitalizelower已经看到了。让我们看一些进一步的例子:

{# prints default value if input is undefined #}
{{ x|default('no opinion') }}
{# prints default value if input evaluates to false #}
{{ none|default('no opinion', true) }}
{# prints input as it was provided #}
{{ 'some opinion'|default('no opinion') }}

{# you can use a filter inside a control statement #}
{# sort by key case-insensitive #}
{% for key in {'A':3, 'b':2, 'C':1}|dictsort %}{{ key }}{% endfor %}
{# sort by key case-sensitive #}
{% for key in {'A':3, 'b':2, 'C':1}|dictsort(true) %}{{ key }}{% endfor %}
{# sort by value #}
{% for key in {'A':3, 'b':2, 'C':1}|dictsort(false, 'value') %}{{ key }}{% endfor %}
{{ [3, 2, 1]|first }} - {{ [3, 2, 1]|last }}
{{ [3, 2, 1]|length }} {# prints input length #}
{# same as in python #}
{{ '%s, =D'|format("I'm John") }}
{{ "He has two daughters"|replace('two', 'three') }}
{# safe prints the input without escaping it first#}
{{ '<input name="stuff" />'|safe }}
{{ "there are five words here"|wordcount }}

尝试前面的例子,以确切了解每个过滤器的作用。

阅读了这么多关于 Jinja2 的内容,您可能会想:“Jinja2 很酷,但这是一本关于 Flask 的书。给我看看 Flask 的东西!”好的,好的,我可以做到!

根据我们迄今所见,几乎一切都可以在 Flask 中使用而无需修改。由于 Flask 为您管理 Jinja2 环境,因此您不必担心创建文件加载程序之类的事情。但是,您应该知道的一件事是,由于您不是自己实例化 Jinja2 环境,因此您实际上无法将要激活的扩展传递给类构造函数。

要激活扩展程序,请在应用程序设置期间将其添加到 Flask 中,如下所示:

from flask import Flask
app = Flask(__name__)
app.jinja_env.add_extension('jinja2.ext.do')  # or jinja2.ext.with_
if __name__ == '__main__':
  app.run()

搞乱模板上下文

在第二章中所见,第一个应用,有多难?,您可以使用render_template方法从templates文件夹加载模板,然后将其呈现为响应。

from flask import Flask, render_template
app = Flask(__name__)

@app.route("/")
def hello():
    return render_template("index.html")

如果您想向模板上下文添加值,就像本章中的一些示例中所示,您将不得不向render_template添加非位置参数:

from flask import Flask, render_template
app = Flask(__name__)

@app.route("/")
def hello():
    return render_template("index.html", my_age=28)

在上面的示例中,my_age将在index.html上下文中可用,其中{{ my_age }}将被翻译为 28。my_age实际上可以具有您想要展示的任何值。

现在,如果您希望所有视图在其上下文中具有特定值,例如版本值-一些特殊代码或函数;您该怎么做?Flask 为您提供了context_processor装饰器来实现这一点。您只需注释一个返回字典的函数,然后就可以开始了。例如:

from flask import Flask, render_response
app = Flask(__name__)

@app.context_processor
def luck_processor():
  from random import randint
  def lucky_number():
    return randint(1, 10)
  return dict(lucky_number=lucky_number)

@app.route("/")
def hello():
  # lucky_number will be available in the index.html context by default
  return render_template("index.html")

总结

在本章中,我们看到了如何仅使用 Jinja2 呈现模板,控制语句的外观以及如何使用它们,如何编写注释,如何在模板中打印变量,如何编写和使用宏,如何加载和使用扩展,以及如何注册上下文处理器。我不知道您怎么看,但这一章节感觉像是大量的信息!我强烈建议您运行示例进行实验。熟悉 Jinja2 将为您节省大量麻烦。

下一章,我们将学习使用 Flask 的表单。期待许多示例和补充代码,因为表单是您从 Web 应用程序打开到 Web 的大门。大多数问题都来自 Web,您的大多数数据也是如此。

第四章:请填写这张表格,夫人

你有没有想象过当你在网站上填写表单并点击最后的漂亮的发送按钮时会发生什么?好吧,你写的所有数据——评论、名称、复选框或其他任何东西——都会被编码并通过协议发送到服务器,然后服务器将这些信息路由到 Web 应用程序。Web 应用程序将验证数据的来源,读取表单,验证数据的语法和语义,然后决定如何处理它。你看到了吗?那里有一长串事件,每个链接都可能是问题的原因?这就是表单。

无论如何,没有什么可害怕的!Flask 可以帮助你完成这些步骤,但也有专门为此目的设计的工具。在本章中,我们将学习:

  • 如何使用 Flask 编写和处理表单

  • 如何验证表单数据

  • 如何使用 WTForms 验证 Flask 中的表单

  • 如何实现跨站点请求伪造保护

这实际上将是一个相当顺利的章节,有很多新信息,但没有复杂的东西。希望你喜欢!

HTML 表单对于胆小的人

HTML 基本上是 Web 编写的语言。借助称为标签的特殊标记,可以为纯文本添加含义和上下文,将其转换为 HTML。对我们来说,HTML 是达到目的的手段。因此,如果你想了解更多,请在你喜欢的浏览器中打开www.w3schools.com/html/。我们没有完全覆盖 HTML 语法,也没有涉及到整个过程中的所有美妙魔法。

虽然我们不会详细介绍 HTML,但我们会专门介绍 HTML;我指的是<form>标签。事实是:每当你打开一个网页,有一些空白字段需要填写时,你很可能在填写 HTML 表单。这是从浏览器向服务器传输数据的最简单方式。这是如何工作的?让我们看一个例子:

<!-- example 1 -->
<form method='post' action='.'>
<input type='text' name='username' />
<input type='password' name='passwd' />
<input type='submit' />
</form>

在上面的例子中,我们有一个完整的登录表单。它的开始由<form>标签定义,具有两个非必需的属性:methodactionmethod属性定义了当发送表单数据时你希望数据如何发送到服务器。它的值可以是getpost。只有当表单数据很小(几百个字符)、不敏感(如果其他人看到它并不重要)且表单中没有文件时,才应该使用get,这是默认值。这些要求存在的原因是,当使用get时,所有表单数据将被编码为参数附加到当前 URL 之后再发送。在我们的例子中,选择的方法是post,因为我们的输入字段之一是密码,我们不希望其他人查看我们的密码。使用get方法的一个很好的用例是搜索表单。例如:

<!-- example 2 -->
<form action='.'>
<input type='search' name='search' />
</form>

示例 2中,我们有一个简单的搜索表单。如果我们在name输入中填写搜索词SearchItem并点击Enter,URL 将如下所示:

mydomain.com/?search=SearchItem

然后,前面的 URL 将保存到浏览器历史记录中,任何有权访问它的人都可以看到上一个用户在搜索什么。对于敏感数据来说,这是不好的。

无论如何,回到示例 1。第二个属性action对于告诉浏览器应该接收和响应表单数据的 URL 非常有用。我们使用'.'作为它的值,因为我们希望表单数据被发送到当前 URL。

接下来的两行是我们的输入字段。输入字段用于收集用户数据,与名称可能暗示的相反,输入字段可以是inputtextareaselect元素。在使用输入字段时,始终记得使用属性name对它们进行命名,因为这有助于在 Web 应用程序中处理它们。

在第三行,我们有一个特殊的输入字段,它不一定有任何要发送的数据,即提交输入按钮。默认情况下,如果在input元素具有焦点时按下Enter,或者按下提交按钮,表单将被发送。我们的示例 1是后者。

哇!终于,我们的表单已经编写和解释完毕。有关输入字段可能类型的详尽列表,请查看www.w3schools.com/tags/tag_input.asp

处理表单

现在让我们看看如何将示例 1中的表单与应用程序集成:

# coding:utf-8

from flask import Flask, render_template, request

app = Flask(__name__)

@app.route('/', methods=['get', 'post'])
def login_view():
    # the methods that handle requests are called views, in flask
    msg = ''

    # form is a dictionary like attribute that holds the form data
    if request.method == 'POST':
      username = request.form["username"]
        passwd = request.form["passwd"]

        # static useless validation
        if username == 'you' and passwd == 'flask':
            msg = 'Username and password are correct'
        else:
            msg = 'Username or password are incorrect'
    return render_template('form.html', message=msg)

if __name__=='__main__':
    app.run()

在前面的例子中,我们定义了一个名为login_view的视图,该视图接受getpost请求;当请求为post时(如果是由get请求发送的表单,则我们忽略该表单),我们获取usernamepasswd的值;然后我们运行一个非常简单的验证,并相应地更改msg的值。

提示

注意:在 Flask 中,视图不同于 MVC 中的视图。在 Flask 中,视图是接收请求并返回响应的组件,可以是函数或类。

您看到我们在示例中处理的request变量了吗?这是当前活动request上下文的代理。这就是为什么request.form指向发送的表单数据。

现在,如果您收到一个编码在 URL 中的参数,您将如何获取它,考虑到请求 URL 是http://localhost:5000/?page=10

# inside a flask view
def some_view():
    try:
        page = int(request.args.get('page', 1))
        assert page == 10
    except ValueError:
        page = 1
    ...

在分页时,前面的例子是非常常见的。与以前一样,request.args只与当前用户请求相关。很简单!

到目前为止,我们用内联验证处理表单验证非常糟糕。不再这样做了!让我们从现在开始尝试一些更花哨的东西。

WTForms 和你

WTForms(github.com/wtforms/wtforms)是一个独立的强大的表单处理库,允许您从类似表单的类生成 HTML 表单,实现字段和表单验证,并包括跨源伪造保护(黑客可能尝试在您的 Web 应用程序中利用的一个恶意漏洞)。我们当然不希望发生这种情况!

首先,要安装 WTForms 库,请使用以下命令:

pip install wtforms

现在让我们编写一些表单。WTForms 表单是扩展Form类的类。就是这么简单!让我们创建一个登录表单,可以与我们之前的登录示例一起使用:

from wtforms import Form, StringField, PasswordField
class LoginForm(Form):
    username = StringField(u'Username:')
    passwd = PasswordField(u'Password:')

在前面的代码中,我们有一个带有两个字段usernamepasswd的表单,没有验证。只需在模板中构建一个表单就足够了,就像这样:

<form method='post'>
{% for field in form %}
    {{ field.label }}
    {{ field }}
    {% if field.errors %}
        {% for error in field.errors %}
            <div class="field_error">{{ error }}</div>
        {% endfor %}
    {% endif %}
{% endfor %}
</form>

如前面的代码所示,您可以迭代 WTForms 表单的字段,每个字段都有一些有用的属性,您可以使用这些属性使您的 HTML 看起来很好,比如labelerrors{{ field }}将为您呈现一个普通的 HTML 输入元素。有些情况下,您可能希望为输入元素设置特殊属性,例如required,告诉浏览器如果为空,则不应提交给定字段。为了实现这一点,调用field作为一个函数,就像这样:

{% if field.flags.required %}
{{ field(required='required') }}
{% endif %}

您可以根据示例传递任何所需的参数,如placeholderalt。Flask-Empty(github.com/italomaia/flask-empty)在其宏中有一个很好的示例。

WTForms 使用标志系统,以允许您检查何时对字段应用了一些验证。如果字段有一个required验证规则,fields.flags属性中的required标志将设置为 true。但是 WTForms 验证是如何工作的呢?

在 Flask 中,验证器是您添加到validators字段的可调用对象,或者是格式为validate_<field>(form, field)的类方法。它允许您验证字段数据是否符合要求,否则会引发ValidationError,解释出了什么问题。让我们看看我们漂亮的登录表单示例如何进行一些验证:

# coding:utf-8
from wtforms import Form, ValidationError
from wtforms import StringField, PasswordField
from wtforms.validators import Length, InputRequired
from werkzeug.datastructures import MultiDict

import re

def is_proper_username(form, field):
    if not re.match(r"^\w+$", field.data):
        msg = '%s should have any of these characters only: a-z0-9_' % field.name
        raise ValidationError(msg)

class LoginForm(Form):
    username = StringField(
        u'Username:', [InputRequired(), is_proper_username, Length(min=3, max=40)])
    password = PasswordField(
        u'Password:', [InputRequired(), Length(min=5, max=12)])

    @staticmethod
    def validate_password(form, field):
        data = field.data
        if not re.findall('.*[a-z].*', data):
            msg = '%s should have at least one lowercase character' % field.name
            raise ValidationError(msg)
        # has at least one uppercase character
        if not re.findall('.*[A-Z].*', data):
            msg = '%s should have at least one uppercase character' % field.name
            raise ValidationError(msg)
        # has at least one number
        if not re.findall('.*[0-9].*', data):
            msg = '%s should have at least one number' % field.name
            raise ValidationError(msg)
        # has at least one special character
        if not re.findall('.*[^ a-zA-Z0-9].*', data):
            msg = '%s should have at least one special character' % field.name
            raise ValidationError(msg)

# testing our form
form = LoginForm(MultiDict([('username', 'italomaia'), ('password', 'lL2m@msbb')]))
print form.validate()
print form.errors

在上述代码中,我们有一个完整的表单示例,带有验证,使用类、方法和函数作为验证器以及一个简单的测试。我们的每个字段的第一个参数是字段标签。第二个参数是在调用form.validate方法时要运行的验证器列表(这基本上就是form.validate做的事情)。每个字段验证器都会按顺序运行,如果发现错误,则会引发ValidationError(并停止验证链调用)。

每个验证器都接收表单和字段作为参数,并必须使用它们进行验证。如validate_password所示,它是因为命名约定而为字段password调用的。field.data保存字段输入,因此您通常可以只验证它。

让我们了解每个验证器:

  • Length:验证输入值的长度是否在给定范围内(最小、最大)。

  • InputRequired:验证字段是否接收到值,任何值。

  • is_proper_username:验证字段值是否与给定的正则表达式匹配。(还有一个内置验证器,用于将正则表达式与给定值匹配,称为Regexp。您应该尝试一下。)

  • validate_password:验证字段值是否符合给定的正则表达式规则组。

在我们的示例测试中,您可能已经注意到了使用werkzeug库中称为MultiDict的特殊类似字典的类。它被使用是因为formdata参数,它可能接收您的request.formrequest.args,必须是multidict-type。这基本上意味着您不能在这里使用普通字典。

调用form.validate时,将调用所有验证器。首先是字段验证器,然后是class方法字段验证器;form.errors是一个字典,其中包含在调用 validate 后找到的所有字段错误。然后您可以对其进行迭代,以在模板、控制台等中显示您找到的内容。

Flask-WTF

Flask 使用扩展以便与第三方库透明集成。WTForms 与 Flask-WTF 是这样的一个很好的例子,我们很快就会看到。顺便说一句,Flask 扩展是一段代码,以可预测的方式与 Flask 集成其配置、上下文和使用。这意味着扩展的使用方式非常相似。现在确保在继续之前在您的虚拟环境中安装了 Flask-WTF:

# oh god, so hard... not!
pip flask-wtf

flask-wtf.readthedocs.org/,项目网站,我们得到了 Flask-WTF 提供的以下功能列表:

  • 与 WTForms 集成

  • 使用 CSRF 令牌保护表单

  • 与 Flask-Uploads 一起工作的文件上传

  • 全局 CSRF 保护

  • Recaptcha 支持

  • 国际化集成

我们将在本章中看到前两个功能,而第三个将在第十章中讨论,现在怎么办?。最后三个功能将不在本书中涵盖。我们建议您将它们作为作业进行探索。

与 WTForms 集成

Flask-WTF 在集成时使用了关于request的小技巧。由于request实现了对当前请求和请求数据的代理,并且在request上下文中可用,扩展Form默认会获取request.form数据,节省了一些输入。

我们的login_view示例可以根据迄今为止讨论的内容进行重写,如下所示:

# make sure you're importing Form from flask_wtf and not wtforms
from flask_wtf import Form

# --//--
@app.route('/', methods=['get', 'post'])
def login_view():
    # the methods that handle requests are called views, in flask
    msg = ''
    # request.form is passed implicitly; implies POST
    form = LoginForm()
    # if the form should also deal with form.args, do it like this:
    # form = LoginForm(request.form or request.args)

    # checks that the submit method is POST and form is valid
    if form.validate_on_submit():
        msg = 'Username and password are correct'
    else:
        msg = 'Username or password are incorrect'
    return render_template('form.html', message=msg)

我们甚至可以更进一步,因为我们显然是完美主义者:

# flash allows us to send messages to the user template without
# altering the returned context
from flask import flash
from flask import redirect
@app.route('/', methods=['get', 'post'])
def login_view():
    # msg is no longer necessary. We will use flash, instead
    form = LoginForm()

    if form.validate_on_submit():
        flash(request, 'Username and password are correct')
        # it's good practice to redirect after a successful form submit
        return redirect('/')
    return render_template('form.html', form=form)

在模板中,将{{ message }}替换为:

{# 
beautiful example from 
http://flask.pocoo.org/docs/0.10/patterns/flashing/#simple-flashing 
#}
{% with messages = get_flashed_messages() %}
  {% if messages %}
    <ul class='messages'>
    {% for message in messages %}
      <li>{{ message }}</li>
    {% endfor %}
    </ul>
  {% endif %}
{% endwith %}

get_flashed_messages默认在模板上下文中可用,并为当前用户提供尚未显示的所有闪现消息。然后我们使用with缓存它,检查它是否不为空,然后对其进行迭代。

提示

闪现消息在重定向时特别有用,因为它们不受响应上下文的限制。

使用 CSRF 令牌保护表单

跨站点请求伪造CSRF)发生在一个网站试图利用另一个网站对你的浏览器的信任(假设你是用户)时。基本上,你正在访问的网站会尝试获取或更改你已经访问并进行身份验证的网站的信息。想象一下,你正在访问一个网站,该网站有一张图片,加载了你已经进行身份验证的另一个网站的 URL;想象一下,给定的 URL 请求了前一个网站的一个动作,并且该动作改变了你的账户的某些内容——例如,它的状态被修改为非活动状态。嗯,这就是 CSRF 攻击的一个简单案例。另一个常见的情况是发送 JSONP 请求。如果被攻击的网站,也就是你没有访问的那个网站,接受 JSONP 表单替换(JSONP 用于跨域请求)并且没有 CRSF 保护,那么你将面临更加恶劣的攻击。

WTForms 自带 CSRF 保护;Flask-WTF 将整个过程与 Flask 粘合在一起,使你的生活更轻松。为了在使用该扩展时具有 CSRF 保护,你需要设置secret_key,就是这样:

app.secret_key = 'some secret string value' # ex: import os; os.urandom(24)

然后,每当你编写一个应该具有 CSRF 保护的表单时,只需确保向其中添加 CSRF 令牌,就像这样:

<form method='post'>{{ form.csrf_token }}
{% for field in form if field.name != 'csrf_token' %}
    <div class="field">
    {{ field.label }} {{ field }}
    </div>
    {% if field.errors %}
        {% for error in field.errors %}
        <div class="field_error">{{ error }}</div>
        {% endfor %}
    {% endif %}
{% endfor %}
<input type='submit' />
</form>

当表单被接收时,CSRF 令牌会与用户会话中注册的内容进行检查。如果它们匹配,表单的来源就是安全的。这是一种安全的方法,因为一个网站无法读取另一个网站设置的 cookie。

在不希望表单受到 CSRF 保护的情况下,不要添加令牌。如果希望取消对表单的保护,必须关闭表单的 CSRF 保护,就像这样:

form = Form(csrf_enabled=False)

在使用get方法但同时又使用表单进行验证的搜索字段的情况下,可能需要取消对表单的保护。

挑战

创建一个 Web 应用程序,接收一个名字,然后回答:“你好,”。如果表单为空发送,应显示错误消息。如果给定的名字是“查克·诺里斯”,答案应该是“旋风踢!”。

创建一个 Web 应用程序,显示一张图片,并询问用户看到了什么。然后应用程序应验证答案是否正确。如果不正确,向用户显示错误消息。否则,祝贺用户并显示一张新图片。使用 Flask-WTF。

创建一个具有四种运算的计算器。它应该有用户可以点击的所有数字和运算符。确保它看起来像一个计算器(因为我们是完美主义者!),并且在用户尝试一些恶意操作时进行投诉,比如将 0 除以 0。

总结

学到了这么多...我能说什么呢!试试看也没什么坏处,对吧?嗯,我们已经学会了如何编写 HTML 表单;使用 Flask 读取表单;编写 WTForms 表单;使用纯 Python 和表单验证器验证表单数据;以及编写自定义验证器。我们还看到了如何使用 Flask-WTF 来编写和验证我们的表单,以及如何保护我们的应用程序免受 CSRF 攻击。

在下一章中,我们将看看如何使用出色、易于使用的库将 Web 应用程序数据存储在关系型和非关系型数据库中,并如何将它们与 Flask 集成。还将进行数据库的简要概述,以便更顺畅地吸收知识。

第五章:你把东西放在哪里?

我就像一只松鼠。我偶尔会在家里的秘密藏匿处留下一些钱,以防我被抢劫,或者在一个月里花费太多。我真的忘记了我所有的藏匿处在哪里,这有点有趣也有点悲哀(对我来说)。

现在,想象一下,你正在存储一些同样重要甚至更重要的东西,比如客户数据或者甚至你公司的数据。你能允许自己将它存储在以后可能会丢失或者可以被某人干扰的地方吗?我们正处于信息时代;信息就是力量!

在网络应用程序世界中,我们有两个大的数据存储玩家:关系数据库NoSQL 数据库。第一种是传统的方式,其中您的数据存储在表和列中,事务很重要,期望有 ACID,规范化是关键(双关语)!它使用SQL来存储和检索数据。在第二种方式中,情况变得有点疯狂。您的数据可能存储在不同的结构中,如文档、图形、键值映射等。写入和查询语言是特定于供应商的,您可能不得不放弃 ACID 以换取速度,大量的速度!

你可能已经猜到了!这一章是关于MVC中的M层,也就是如何以透明的方式存储和访问数据的章节!我们将看一下如何使用查询和写入两种数据库类型的示例,以及何时选择使用哪种。

提示

ACID 是原子性、一致性、隔离性和持久性的缩写。请参考en.wikipedia.org/wiki/ACID了解一个舒适的定义和概述。

SQLAlchemy

SQLAlchemy 是一个与关系数据库一起工作的惊人库。它是由 Pocoo 团队制作的,他们也是 Flask 的创始人,被认为是“事实上”的 Python SQL 库。它可以与 SQLite、Postgres、MySQL、Oracle 和所有 SQL 数据库一起使用,这些数据库都有兼容的驱动程序。

SQLite 自称为一个自包含、无服务器、零配置和事务性 SQL 数据库引擎(sqlite.org/about.html)。其主要目标之一是成为应用程序和小型设备的嵌入式数据库解决方案,它已经做到了!它也非常容易使用,这使得它非常适合我们的学习目的。

尽管所有的例子都将以 SQLite 为主要考虑对象进行给出和测试,但它们应该在其他数据库中也能够以很少或没有改动的方式工作。在适当的时候,将会不时地给出特定于数据库的提示。

注意

请参考www.w3schools.com/sql/default.asp了解广泛的 SQL 参考。

在我们的第一个例子之前,我们是否应该复习一下几个关系数据库的概念?

概念

是低级抽象结构,用于存储数据。它由组成,其中每一列代表数据的一部分,每一行代表一个完整的记录。通常,每个表代表一个类模型的低级抽象。

是给定类模型的单个记录。您可能需要将多个行记录分散到不同的表中,以记录完整的信息。一个很好的例子是MxN 关系

代表存储的数据本身。每一列都有一个特定的类型,并且只接受该类型的输入数据。您可以将其视为类模型属性的抽象。

事务是用来将要执行的操作分组的方式。它主要用于实现原子性。这样,没有操作是半途而废的。

主键是一个数据库概念,记录的一部分数据用于标识数据库表中的给定记录。通常由数据库通过约束来实现。

外键是一个数据库概念,用于在不同表之间标识给定记录的一组数据。它的主要用途是在不同表的行之间构建关系。通常由数据库通过约束来实现。

在使用关系数据库时的一个主要关注点是数据规范化。在关系数据库中,相关数据存储在不同的表中。您可能有一个表来保存一个人的数据,一个表来保存这个人的地址,另一个表来保存他/她的汽车,等等。

每个表都与其他表隔离,通过外键建立的关系可以检索相关数据!数据规范化技术是一组规则,用于允许数据在表之间适当分散,以便轻松获取相关表,并将冗余保持最小。

提示

请参考en.wikipedia.org/wiki/Database_normalization了解数据库规范化的概述。

有关规范形式的概述,请参阅以下链接:

en.wikipedia.org/wiki/First_normal_form

en.wikipedia.org/wiki/Second_normal_form

en.wikipedia.org/wiki/Third_normal_form

我们现在可以继续了!

实际操作

让我们开始将库安装到我们的环境中,并尝试一些示例:

pip install sqlalchemy

我们的第一个示例!让我们为一家公司(也许是你的公司?)创建一个简单的员工数据库:

from sqlalchemy import create_engine
db = create_engine('sqlite:///employees.sqlite')
# echo output to console
db.echo = True

conn = db.connect()

conn.execute("""
CREATE TABLE employee (
  id          INTEGER PRIMARY KEY,
  name        STRING(100) NOT NULL,
  birthday    DATE NOT NULL
)""")

conn.execute("INSERT INTO employee VALUES (NULL, 'marcos mango', date('1990-09-06') );")
conn.execute("INSERT INTO employee VALUES (NULL, 'rosie rinn', date('1980-09-06') );")
conn.execute("INSERT INTO employee VALUES (NULL, 'mannie moon', date('1970-07-06') );")
for row in conn.execute("SELECT * FROM employee"):
    print row
# give connection back to the connection pool
conn.close()

前面的例子非常简单。我们创建了一个 SQLAlchemy 引擎,从连接池中获取连接(引擎会为您处理),然后执行 SQL 命令来创建表,插入几行数据并查询是否一切都如预期发生。

提示

访问en.wikipedia.org/wiki/Connection_pool了解连接池模式概述。(这很重要!)

在我们的插入中,我们为主键id提供了值NULL。请注意,SQLite 不会使用NULL填充主键;相反,它会忽略NULL值,并将列设置为新的、唯一的整数。这是 SQLite 特有的行为。例如,Oracle将要求您显式插入序列的下一个值,以便为主键设置一个新的唯一列值。

我们之前的示例使用了一个名为autocommit的功能。这意味着每次执行方法调用都会立即提交到数据库。这样,您无法一次执行多个语句,这在现实世界的应用程序中是常见的情况。

要一次执行多个语句,我们应该使用事务。我们可以通过事务重写我们之前的示例,以确保所有三个插入要么一起提交,要么根本不提交(严肃的表情...)。

# we start our transaction here
# all actions now are executed within the transaction context
trans = conn.begin()

try:
    # we are using a slightly different insertion syntax for convenience, here; 
    # id value is not explicitly provided
    conn.execute("INSERT INTO employee (name, birthday) VALUES ('marcos mango', date('1990-09-06') );")
    conn.execute("INSERT INTO employee (name, birthday) VALUES ('rosie rinn', date('1980-09-06') );")
    conn.execute("INSERT INTO employee (name, birthday) VALUES ('mannie moon', date('1970-07-06') );")
    # commit all
    trans.commit()
except:
    # all or nothing. Undo what was executed within the transaction
    trans.rollback()
    raise

到目前为止还没有什么花哨的。在我们的例子中,我们从连接创建了一个事务,执行了一些语句,然后提交以完成事务。如果在事务开始和结束之间发生错误,except块将被执行,并且在事务中执行的所有语句将被回滚或“撤消”。

我们可以通过在表之间创建关系来完善我们的示例。想象一下,我们的员工在公司档案中注册了一个或多个地址。我们将创建一个 1xN 关系,其中一个员工可以拥有一个或多个地址。

# coding:utf-8
from sqlalchemy import create_engine

engine = create_engine('sqlite:///employees.sqlite')
engine.echo = True

conn = engine.connect()

conn.execute("""
CREATE TABLE employee (
  id          INTEGER PRIMARY KEY,
  name        STRING(100) NOT NULL,
  birthday    DATE NOT NULL
)""")

conn.execute("""
CREATE TABLE address(
  id      INTEGER PRIMARY KEY,
  street  STRING(100) NOT NULL,
  number  INTEGER,
  google_maps STRING(255),
  id_employee INTEGER NOT NULL,
  FOREIGN KEY(id_employee) REFERENCES employee(id)
)""")

trans = conn.begin()
try:
    conn.execute("INSERT INTO employee (name, birthday) VALUES ('marcos mango', date('1990-09-06') );")
    conn.execute("INSERT INTO employee (name, birthday) VALUES ('rosie rinn', date('1980-09-06') );")
    conn.execute("INSERT INTO employee (name, birthday) VALUES ('mannie moon', date('1970-07-06') );")
    # insert addresses for each employee
    conn.execute(
        "INSERT INTO address (street, number, google_maps, id_employee) "
        "VALUES ('Oak', 399, '', 1)")
    conn.execute(
        "INSERT INTO address (street, number, google_maps, id_employee) "
        "VALUES ('First Boulevard', 1070, '', 1)")
    conn.execute(
        "INSERT INTO address (street, number, google_maps, id_employee) "
        "VALUES ('Cleveland, OH', 10, 'Cleveland,+OH,+USA/@41.4949426,-81.70586,11z', 2)")
    trans.commit()
except:
    trans.rollback()
    raise

# get marcos mango addresses
for row in conn.execute("""
  SELECT a.street, a.number FROM employee e
  LEFT OUTER JOIN address a
  ON e.id = a.id_employee
  WHERE e.name like '%marcos%';
  """):
    print "address:", row
conn.close()

在我们新的和更新的示例中,我们记录了一些员工的地址,确保使用正确的外键值(id_employee),然后我们使用LEFT JOIN查找名为'marcos mango'的员工的地址。

我们已经看到了如何创建表和关系,运行语句来查询和插入数据,并使用 SQLAlchemy 进行事务处理;我们还没有完全探索 SQLAlchemy 库的强大功能。

SQLAlchemy 具有内置的 ORM,允许您像使用本机对象实例一样使用数据库表。想象一下,读取列值就像读取实例属性一样,或者通过方法查询复杂的表关系,这就是 SQLAlchemy 的 ORM。

让我们看看使用内置 ORM 的示例会是什么样子:

# coding:utf-8

from sqlalchemy import create_engine
from sqlalchemy import Column, Integer, String, Date, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.ext.declarative import declarative_base

from datetime import datetime

engine = create_engine('sqlite:///employees.sqlite')
engine.echo = True

# base class for our models
Base = declarative_base()

# we create a session binded to our engine
Session = sessionmaker(bind=engine)

# and then the session itself
session = Session()

# our first model
class Address(Base):
    # the table name we want in the database
    __tablename__ = 'address'

    # our primary key
    id = Column(Integer, primary_key=True)
    street = Column(String(100))
    number = Column(Integer)
    google_maps = Column(String(255))
    # our foreign key to employee
    id_employee = Column(Integer, ForeignKey('employee.id'))

    def __repr__(self):
         return u"%s, %d" % (self.street, self.number)

class Employee(Base):
    __tablename__ = 'employee'

    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    birthday = Column(Date)
    # we map 
    addresses = relationship("Address", backref="employee")

    def __repr__(self):
         return self.name

# create our database from our classes
Base.metadata.create_all(engine)

# execute everything inside a transaction
session.add_all([
        Employee(name='marcos mango', birthday=datetime.strptime('1990-09-06', '%Y-%m-%d')), 
        Employee(name='rosie rinn', birthday=datetime.strptime('1980-09-06', '%Y-%m-%d')),
        Employee(name='mannie moon', birthday=datetime.strptime('1970-07-06', '%Y-%m-%d'))
    ])
session.commit()

session.add_all([
    Address(street='Oak', number=399, google_maps='', id_employee=1),
    Address(street='First Boulevard', number=1070, google_maps='', id_employee=1),
    Address(street='Cleveland, OH', number=10, 
             google_maps='Cleveland,+OH,+USA/@41.4949426,-81.70586,11z', id_employee=2)
])
session.commit()

# get marcos, then his addresses
marcos = session.query(Employee).filter(Employee.name.like(r"%marcos%")).first()
for address in marcos.addresses:
    print 'Address:', address

前面的示例介绍了相当多的概念。首先,我们创建了我们的引擎,即第一个示例中使用的 SQLAlchemy 引擎,然后我们创建了我们的基本模型类。虽然Employee将被create_all映射到一个名为employee的表中,但每个定义的Column属性都将被映射到数据库中给定表的列中,并具有适当的约束。例如,对于id字段,它被定义为主键,因此将为其创建主键约束。id_employee是一个外键,它是对另一个表的主键的引用,因此它将具有外键约束,依此类推。

我们所有的类模型都应该从中继承。然后我们创建一个session。会话是您使用 SQLAlchemy ORM 模型的方式。

会话具有内部正在进行的事务,因此它非常容易具有类似事务的行为。它还将您的模型映射到正确的引擎,以防您使用多个引擎;但等等,还有更多!它还跟踪从中加载的所有模型实例。例如,如果您将模型实例添加到其中,然后修改该实例,会话足够聪明,能够意识到其对象的更改。因此,它会将自身标记为脏(内容已更改),直到调用提交或回滚。

在示例中,在找到 marcos 之后,我们可以将"Marcos Mango's"的名字更改为其他内容,比如"marcos tangerine",就像这样:

marcos.name = "marcos tangerine"
session.commit()

现在,在Base.metadata之后注释掉整个代码,并添加以下内容:

marcos = session.query(Employee).filter(Employee.name.like(r"%marcos%")).first()
marcos_last_name = marcos.name.split(' ')[-1]
print marcos_last_name

现在,重新执行示例。Marcos 的新姓氏现在是"tangerine"。神奇!

提示

有关使用 SQLAlchemy ORM 进行查询的惊人、超级、强大的参考,请访问docs.sqlalchemy.org/en/rel_0_9/orm/tutorial.html#querying

在谈论了这么多关于 SQLAlchemy 之后,您能否请醒来,因为我们将谈论 Flask-SQLAlchemy,这个扩展将库与 Flask 集成在一起。

Flask-SQLAlchemy

Flask-SQLAlchemy 是一个轻量级的扩展,它将 SQLAlchemy 封装在 Flask 周围。它允许您通过配置文件配置 SQLAlchemy 引擎,并为每个请求绑定一个会话,为您提供了一种透明的处理事务的方式。让我们看看如何做到这一点。首先,确保我们已经安装了所有必要的软件包。加载虚拟环境后,运行:

pip install flask-wtf flask-sqlalchemy

我们的代码应该是这样的:

# coding:utf-8
from flask import Flask, render_template, redirect, flash
from flask_wtf import Form
from flask.ext.sqlalchemy import SQLAlchemy

from wtforms.ext.sqlalchemy.orm import model_form

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/employees.sqlite'
app.config['SQLALCHEMY_ECHO'] = True

# initiate the extension
db = SQLAlchemy(app)

# define our model
class Employee(db.Model):
    __tablename__ = 'employee'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    birthday = db.Column(db.Date, nullable=False)

    def __repr__(self):
        return 'employee %s' % self.name

# create the database
db.create_all()

# auto-generate form for our model
EmployeeForm = model_form(Employee, base_class=Form, field_args={
    'name': {
    'class': 'employee'
  }
})

@app.route("/", methods=['GET', 'POST'])
def index():
    # as you remember, request.POST is implicitly provided as argument
    form = EmployeeForm()

    try:
        if form.validate_on_submit():
            employee = Employee()
            form.populate_obj(employee)
            db.session.add(employee)
            db.session.commit()
            flash('New employee add to database')
            return redirect('/')
    except Exception, e:
        # log e
        db.session.rollback()
        flash('An error occurred accessing the database. Please, contact administration.')

    employee_list=Employee.query.all()
    return render_template('index.html', form=form, employee_list=employee_list)

if __name__ == '__main__':
    app.debug = True
    app.run()

前面的示例非常完整。它具有表单验证、CSRF 保护、从模型自动生成的表单以及数据库集成。让我们只关注到目前为止我们还没有提到的内容。

自动生成表单非常方便。使用model_form,您可以自省定义的模型类并生成适合该模型的表单类。您还可以通过model_form参数field_args为字段提供参数,这对于添加元素类或额外验证器非常有用。

您可能还注意到Employee扩展了db.Model,这是您的 ORM 模型基类。所有您的模型都应该扩展它,以便被db所知,它封装了我们的引擎并保存我们的请求感知会话。

在 index 函数内部,我们实例化表单,然后检查它是否通过 POST 提交并且有效。在if块内部,我们实例化我们的员工模型,并使用populate_obj将表单的值放入模型实例中。我们也可以逐个字段地进行操作,就像这样:

employee.name = form.name.data
employee. birthday = form.birthday.data

populate_obj只是更方便。在填充模型后,我们将其添加到会话中以跟踪它,并提交会话。在此块中发生任何异常时,我们将其放在一个带有准备回滚的 try/except 块中。

请注意,我们使用Employee.query来查询存储在我们数据库中的员工。每个模型类都带有一个query属性,允许您从数据库中获取和过滤结果。对query的每个过滤调用将返回一个BaseQuery实例,允许您堆叠过滤器,就像这样:

queryset = Employee.query.filter_by(name='marcos mango')
queryset = queryset.filter_by(birthday=datetime.strptime('1990-09-06', '%Y-%m-%d'))
queryset.all()  # <= returns the result of both filters applied together

这里有很多可能性。为什么不现在就尝试一些例子呢?

注意

与 Web 应用程序和数据库相关的最常见的安全问题是SQL 注入攻击,攻击者将 SQL 指令注入到您的数据库查询中,获取他/她不应该拥有的权限。SQLAlchemy 的引擎对象“自动”转义您的查询中的特殊字符;因此,除非您明确绕过其引用机制,否则您应该是安全的。

MongoDB

MongoDB 是一个广泛使用的强大的 NoSQL 数据库。它允许您将数据存储在文档中;一个可变的、类似字典的、类似对象的结构,您可以在其中存储数据,而无需担心诸如“我的数据是否规范化到第三范式?”或“我是否必须创建另一个表来存储我的关系?”等问题。

MongoDB 文档实际上是 BSON 文档,是 JSON 的超集,支持扩展的数据类型。如果您知道如何处理 JSON 文档,您应该不会有问题。

提示

如果 JSON 对您毫无意义,只需查看www.w3schools.com/json/

让我们在本地安装 MongoDB,以便尝试一些例子:

sudo apt-get install mongodb

现在,从控制台输入:

mongo

您将进入 MongoDB 交互式控制台。从中,您可以执行命令,向数据库添加文档,查询、更新或删除。您可以通过控制台实现的任何语法,也可以通过控制台实现。现在,让我们了解两个重要的 MongoDB 概念:数据库和集合。

在 MongoDB 中,您的文档被分组在集合内,而集合被分组在数据库内。因此,在连接到 MongoDB 后,您应该做的第一件事是选择要使用的数据库。您不需要创建数据库,连接到它就足以创建数据库。对于集合也是一样。您也不需要在使用文档之前定义其结构,也不需要实现复杂的更改命令,如果您决定文档结构应该更改。这里有一个例子:

> use example
switched to db example
> db.employees.insert({name: 'marcos mango', birthday: new Date('Sep 06, 1990')})
WriteResult({ "nInserted" : 1 })
> db.employees.find({'name': {$regex: /marcos/}})

在上述代码中,我们切换到示例数据库,然后将一个新文档插入到员工集合中(我们不需要在使用之前创建它),最后,我们使用正则表达式搜索它。MongoDB 控制台实际上是一个 JavaScript 控制台,因此新的Date实际上是 JavaScript 类Date的实例化。非常简单。

提示

如果您不熟悉 JavaScript,请访问www.w3schools.com/js/default.asp了解一个很好的概述。

我们可以存储任何 JSON 类型的文档,还有其他一些类型。访问docs.mongodb.org/manual/reference/bson-types/获取完整列表。

关于正确使用 MongoDB,只需记住几个黄金规则:

  • 避免将数据从一个集合保留到另一个集合,因为 MongoDB 不喜欢连接

  • 在 MongoDB 中,将文档值作为列表是可以的,甚至是预期的

  • 在 MongoDB 中,适当的文档索引(本书未涉及)对性能至关重要

  • 写入比读取慢得多,可能会影响整体性能

MongoEngine

MongoEngine 是一个非常棒的 Python 库,用于访问和操作 MongoDB 文档,并使用PyMongo,MongoDB 推荐的 Python 库。

提示

由于 PyMongo 没有文档对象映射器DOM),我们不直接使用它。尽管如此,有些情况下 MongoEngine API 将不够用,您需要使用 PyMongo 来实现您的目标。

它有自己的咨询 API 和文档到类映射器,允许您以与使用 SQLAlchemy ORM 类似的方式处理文档。这是一个好事,因为 MongoDB 是无模式的。它不像关系数据库那样强制执行模式。这样,您在使用之前不必声明文档应该是什么样子。MongoDB 根本不在乎!

在实际的日常开发中,确切地知道您应该在文档中存储什么样的信息是一个很好的反疯狂功能,MongoEngine 可以直接为您提供。

由于您的机器上已经安装了 MongoDB,只需安装 MongoEngine 库即可开始使用它编码:

pip install mongoengine pymongo==2.8

让我们使用我们的新库将“Rosie Rinn”添加到数据库中:

# coding:utf-8

from mongoengine import *
from datetime import datetime

# as the mongo daemon, mongod, is running locally, we just need the database name to connect
connect('example')

class Employee(Document):
    name = StringField()
    birthday = DateTimeField()

    def __unicode__(self):
        return u'employee %s' % self.name

employee = Employee()
employee.name = 'rosie rinn'
employee.birthday = datetime.strptime('1980-09-06', '%Y-%m-%d')
employee.save()

for e in Employee.objects(name__contains='rosie'):
    print e

理解我们的示例:首先,我们使用example数据库创建了一个 MongoDB 连接,然后像使用 SQLAlchemy 一样定义了我们的员工文档,最后,我们插入了我们的员工“Rosie”并查询是否一切正常。

在声明我们的Employee类时,您可能已经注意到我们必须使用适当的字段类型定义每个字段。如果 MongoDB 是无模式的,为什么会这样?MongoEngine 强制执行每个模型字段的类型。如果您为模型定义了IntField并为其提供了字符串值,MongoEngine 将引发验证错误,因为那不是适当的字段值。此外,我们为Employee定义了一个__unicode__方法,以便在循环中打印员工的姓名。__repr__在这里不起作用。

由于 MongoDB 不支持事务(MongoDB 不是 ACID,记住?),MongoEngine 也不支持,我们进行的每个操作都是原子的。当我们创建我们的“Rosie”并调用save方法时,“Rosie”立即插入数据库;不需要提交更改或其他任何操作。

最后,我们有数据库查询,我们搜索“Rosie”。要查询所选集合,应使用每个 MongoEngine 文档中可用的objects处理程序。它提供了类似 Django 的界面,支持操作,如containsicontainsnelte等。有关查询运算符的完整列表,请访问mongoengine-odm.readthedocs.org/guide/querying.html#query-operators

Flask-MongoEngine

MongoEngine 本身非常容易,但有人认为事情可以变得更好,于是我们有了 Flask-MongoEngine。它为您提供了三个主要功能:

  • Flask-DebugToolbar 集成(嘿嘿!)

  • 类似 Django 的查询集(get_or_404first_or_404paginatepaginate_field

  • 连接管理

Flask-DebugToolbar 是一个漂亮的 Flask 扩展,受到 Django-DebugToolbar 扩展的启发,它跟踪应用程序在幕后发生的事情,例如请求中使用的 HTTP 标头,CPU 时间,活动 MongoDB 连接的数量等。

类似 Django 的查询是一个很有用的功能,因为它们可以帮助你避免一些无聊的编码。get_or_404(*args, **kwargs)查询方法会在未找到要查找的文档时引发 404 HTTP 页面(它在内部使用get)。如果你正在构建一个博客,你可能会喜欢在加载特定的文章条目时使用这个小家伙。first_or_404()查询方法类似,但适用于集合。如果集合为空,它会引发 404 HTTP 页面。paginate(page, per_page)查询实际上是一个非常有用的查询方法。它为你提供了一个开箱即用的分页界面。它在处理大型集合时效果不佳,因为在这些情况下 MongoDB 需要不同的策略,但大多数情况下,它就是你所需要的。paginate_field(field_name, doc_id, page, per_page)是 paginate 的更具体版本,因为你将对单个文档字段进行分页,而不是对集合进行分页。当你有一个文档,其中一个字段是一个巨大的列表时,它非常有用。

现在,让我们看一个完整的flask-mongoengine示例。首先,在我们的虚拟环境中安装这个库:

pip install flask-mongoengine

现在开始编码:

# coding:utf-8

from flask import Flask, flash, redirect, render_template
from flask.ext.mongoengine import MongoEngine
from flask.ext.mongoengine.wtf import model_form
from flask_wtf import Form

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['MONGODB_SETTINGS'] = {
    # 'replicaset': '',
    'db': 'example',
    # 'host': '',
    # 'username': '',
    # 'password': ''
}
db = MongoEngine(app)

class Employee(db.Document):
    name = db.StringField()
    # mongoengine does not support datefield
    birthday = db.DateTimeField()

    def __unicode__(self):
        return u'employee %s' % self.name

# auto-generate form for our model
EmployeeForm = model_form(Employee, base_class=Form, field_args={
    'birthday': {
        # we want to use date format, not datetime
        'format': '%Y-%m-%d'
    }
})

@app.route("/", methods=['GET', 'POST'])
def index():
    # as you remember, request.POST is implicitly provided as argument
    form = EmployeeForm()

    try:
        if form.validate_on_submit():
            employee = Employee()
            form.populate_obj(employee)
            employee.save()
            flash('New employee add to database')
            return redirect('/')
    except:
        # log e
        flash('An error occurred accessing the database. Please, contact administration.')

    employee_list=Employee.objects()
    return render_template('index.html', form=form, employee_list=employee_list)

if __name__ == '__main__':
    app.debug = True
    app.run()

我们的 Flask-MongoEngine 示例与 Flask-SQLAlchemy 示例非常相似。除了导入的差异之外,还有 MongoDB 的配置,因为 MongoDB 需要不同的参数;我们有birthday字段类型,因为 MongoEngine 不支持DateField;有生日格式的覆盖,因为datetimefield的默认字符串格式与我们想要的不同;还有index方法的更改。

由于我们不需要使用 Flask-MongoEngine 处理会话,我们只需删除所有与它相关的引用。我们还改变了employee_list的构建方式。

提示

由于 MongoDB 不会解析你发送给它的数据以尝试弄清楚查询的内容,所以你不会遇到 SQL 注入的问题。

关系型与 NoSQL

你可能会想知道何时使用关系型数据库,何时使用 NoSQL。嗯,鉴于今天存在的技术和技术,我建议你选择你感觉更适合的类型来工作。NoSQL 吹嘘自己是无模式、可扩展、快速等,但关系型数据库对于大多数需求也是相当快速的。一些关系型数据库,比如 Postgres,甚至支持文档。那么扩展呢?嗯,大多数项目不需要扩展,因为它们永远不会变得足够大。其他一些项目,只需与它们的关系型数据库一起扩展。

如果没有重要的原因来选择原生无模式支持或完整的 ACID 支持,它们中的任何一个都足够好。甚至在安全方面,也没有值得一提的大差异。MongoDB 有自己的授权方案,就像大多数关系型数据库一样,如果配置正确,它们都是一样安全的。通常,应用层在这方面更加麻烦。

摘要

这一章非常紧凑!我们对关系型和 NoSQL 数据库进行了概述,学习了 MongoDB 和 MongoEngine,SQLite 和 SQLAlchemy,以及如何使用扩展来将 Flask 与它们集成。知识积累得很快!现在你能够创建更复杂的带有数据库支持、自定义验证、CSRF 保护和用户通信的网络应用程序了。

在下一章中,我们将学习关于 REST 的知识,它的优势,以及如何创建服务供应用程序消费。

第六章:但是我现在想休息妈妈!

REST 是一种架构风格,由于其许多特性和架构约束(如可缓存性、无状态行为和其接口要求),近年来一直在获得动力。

提示

有关 REST 架构的概述,请参阅www.drdobbs.com/Web-development/restful-Web-services-a-tutorial/240169069en.wikipedia.org/wiki/Representational_state_transfer

本章我们将专注于 RESTful Web 服务和 API——即遵循 REST 架构的 Web 服务和 Web API。让我们从开始说起:什么是 Web 服务?

Web 服务是一个可以被你的应用程序查询的 Web 应用程序,就像它是一个 API 一样,提高了用户体验。如果你的 RESTful Web 服务不需要从传统的 UI 界面调用,并且可以独立使用,那么你拥有的是一个RESTful Web 服务 API,简称“RESTful API”,它的工作方式就像一个常规 API,但通过 Web 服务器。

对 Web 服务的调用可能会启动批处理过程、更新数据库或只是检索一些数据。对服务可能执行的操作没有限制。

RESTful Web 服务应该通过URI(类似于 URL)访问,并且可以通过任何 Web 协议访问,尽管HTTP在这里是王者。因此,我们将专注于HTTP。我们的 Web 服务响应,也称为资源,可以具有任何所需的格式;如 TXT、XML 或 JSON,但最常见的格式是 JSON,因为它非常简单易用。我们还将专注于 JSON。在使用 HTTP 与 Web 服务时,一种常见的做法是使用 HTTP 默认方法(GETPOSTPUTDELETEOPTIONS)向服务器提供关于我们想要实现的更多信息。这种技术允许我们在同一个服务中拥有不同的功能。

http://localhost:5000/age的服务调用可以通过GET请求返回用户的年龄,或通过DELETE请求删除其值。

让我们看看每个通常使用的方法通常用于什么:

  • GET:这用于检索资源。你想要信息?不需要更新数据库?使用 GET!

  • POST:这用于将新数据插入服务器,比如在数据库中添加新员工。

  • PUT:这用于更新服务器上的数据。你有一个员工决定在系统中更改他的昵称?使用PUT来做到这一点!

  • DELETE:这是你在服务器上删除数据的最佳方法!

  • OPTIONS:这用于询问服务支持哪些方法。

到目前为止,有很多理论;让我们通过一个基于 Flask 的 REST Web 服务示例来实践。

首先,安装示例所需的库:

pip install marshmallow

现在,让我们来看一个例子:

# coding:utf-8

from flask import Flask, jsonify
from flask.ext.sqlalchemy import SQLAlchemy

from marshmallow import Schema

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/articles.sqlite'

db = SQLAlchemy(app)

class Article(db.Model):
    __tablename__ = 'articles'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text(), nullable=False)

    def __unicode__(self):
        return self.content

# we use marshmallow Schema to serialize our articles
class ArticleSchema(Schema):
    """
    Article dict serializer
    """
    class Meta:
        # which fields should be serialized?
        fields = ('id', 'title', 'content')

article_schema = ArticleSchema()
# many -> allow for object list dump
articles_schema = ArticleSchema(many=True)

@app.route("/articles/", methods=["GET"])
@app.route("/articles/<article_id>", methods=["GET"])
def articles(article_id=None):
    if article_id:
        article = Article.query.get(article_id)

        if article is None:
            return jsonify({"msgs": ["the article you're looking for could not be found"]}), 404

        result = article_schema.dump(article)
        return jsonify({'article': result})
    else:
        # never return the whole set! As it would be very slow
        queryset = Article.query.limit(10)
        result = articles_schema.dump(queryset)

        # jsonify serializes our dict into a proper flask response
        return jsonify({"articles": result.data})

db.create_all()

# let's populate our database with some data; empty examples are not that cool
if Article.query.count() == 0:
    article_a = Article(title='some title', content='some content')
    article_b = Article(title='other title', content='other content')

    db.session.add(article_a)
    db.session.add(article_b)
    db.session.commit()

if __name__ == '__main__':
    # we define the debug environment only if running through command line
    app.config['SQLALCHEMY_ECHO'] = True
    app.debug = True
    app.run()

在前面的示例中,我们创建了一个 Web 服务,使用 GET 请求来查询文章。引入了jsonify函数,因为它用于将 Python 对象序列化为 Flask JSON 响应。我们还使用 marshmallow 库将 SQLAlchemy 结果序列化为 Python 字典,因为没有原生 API 可以做到这一点。

让我们逐步讨论这个例子:

首先,我们创建我们的应用程序并配置我们的 SQLAlchemy 扩展。然后定义Article模型,它将保存我们的文章数据,以及一个 ArticleSchema,它允许 marshmallow 将我们的文章序列化。我们必须在 Schema Meta 中定义应该序列化的字段。article_schema是我们用于序列化单篇文章的模式实例,而articles_schema序列化文章集合。

我们的文章视图有两个定义的路由,一个用于文章列表,另一个用于文章详情,返回单篇文章。

在其中,如果提供了article_id,我们将序列化并返回请求的文章。如果数据库中没有与article_id对应的记录,我们将返回一个带有给定错误和 HTTP 代码 404 的消息,表示“未找到”状态。如果article_idNone,我们将序列化并返回 10 篇文章。您可能会问,为什么不返回数据库中的所有文章?如果我们在数据库中有 10,000 篇文章并尝试返回那么多,我们的服务器肯定会出问题;因此,避免返回数据库中的所有内容。

这种类型的服务通常由使用 JavaScript(如 jQuery 或 PrototypeJS)的 Ajax 请求来消耗。在发送 Ajax 请求时,这些库会添加一个特殊的标头,使我们能够识别给定请求是否实际上是 Ajax 请求。在我们的前面的例子中,我们为所有 GET 请求提供 JSON 响应。

提示

不懂 Ajax?访问www.w3schools.com/Ajax/ajax_intro.asp

我们可以更加选择性,只对 Ajax 请求发送 JSON 响应。常规请求将收到纯 HTML 响应。要做到这一点,我们需要对视图进行轻微更改,如下所示:

from flask import request
…

@app.route("/articles/", methods=["GET"])
@app.route("/articles/<article_id>", methods=["GET"])
def articles(article_id=None):
    if article_id:
        article = Article.query.get(article_id)

        if request.is_xhr:
            if article is None:
                return jsonify({"msgs": ["the article you're looking for could not be found"]}), 404

            result = article_schema.dump(article)
            return jsonify({'article': result})
        else:
            if article is None:
                abort(404)

            return render_template('article.html', article=article)
    else:
        queryset = Article.query.limit(10)

        if request.is_xhr:
            # never return the whole set! As it would be very slow
            result = articles_schema.dump(queryset)

            # jsonify serializes our dict into a proper flask response
            return jsonify({"articles": result.data})
        else:
            return render_template('articles.html', articles=queryset)

request对象有一个名为is_xhr的属性,您可以检查该属性以查看请求是否实际上是 Ajax 请求。如果我们将前面的代码拆分成几个函数,例如一个用于响应 Ajax 请求,另一个用于响应纯 HTTP 请求,那么我们的前面的代码可能会更好。为什么不尝试重构代码呢?

我们的最后一个示例也可以采用不同的方法;我们可以通过 Ajax 请求加载所有数据,而不向其添加上下文变量来呈现 HTML 模板。在这种情况下,需要对代码进行以下更改:

from marshmallow import Schema, fields
class ArticleSchema(Schema):
    """
      Article dict serializer
      """
      url = fields.Method("article_url")
      def article_url(self, article):
          return article.url()

      class Meta:
          # which fields should be serialized?
          fields = ('id', 'title', 'content', 'url')

@app.route("/articles/", methods=["GET"])
@app.route("/articles/<article_id>", methods=["GET"])
def articles(article_id=None):
    if article_id:
        if request.is_xhr:
            article = Article.query.get(article_id)
            if article is None:
                return jsonify({"msgs": ["the article you're looking for could not be found"]}), 404

            result = article_schema.dump(article)
            return jsonify({'article': result})
        else:
            return render_template('article.html')
    else:
        if request.is_xhr:
            queryset = Article.query.limit(10)
            # never return the whole set! As it would be very slow
            result = articles_schema.dump(queryset)

            # jsonify serializes our dict into a proper flask response
            return jsonify({"articles": result.data})
        else:
            return render_template('articles.html')

我们在模式中添加了一个新字段url,以便从 JavaScript 代码中访问文章页面的路径,因为我们返回的是一个 JSON 文档而不是 SQLAlchemy 对象,因此无法访问模型方法。

articles.html文件将如下所示:

<!doctype html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Articles</title>
</head>
<body>
<ul id="articles">
</ul>

<script type="text/javascript" src="img/jquery-2.1.3.min.js"></script>
<script type="text/javascript">
  // only execute after loading the whole HTML
  $(document).ready(function(){
    $.ajax({
      url:"{{ url_for('.articles') }}",
      success: function(data, textStatus, xhr){
        $(data['articles']).each(function(i, el){
          var link = "<a href='"+ el['url'] +"'>" + el['title'] + "</a>";
          $("#articles").append("<li>" + link + "</li>");
        });}});}); // don't do this in live code
</script>
</body>
</html>

在我们的模板中,文章列表是空的;然后在使用 Ajax 调用我们的服务后进行填充。如果您测试完整的示例,Ajax 请求非常快,您甚至可能都没有注意到页面在填充 Ajax 之前是空的。

超越 GET

到目前为止,我们已经有了一些舒适的 Ajax 和 RESTful Web 服务的示例,但我们还没有使用服务将数据记录到我们的数据库中。现在试试吧?

使用 Web 服务记录到数据库与我们在上一章中所做的并没有太大的不同。我们将从 Ajax 请求中接收数据,然后检查使用了哪种 HTTP 方法以决定要做什么,然后我们将验证发送的数据并保存所有数据(如果没有发现错误)。在第四章请填写这张表格,夫人中,我们谈到了 CSRF 保护及其重要性。我们将继续使用我们的 Web 服务对数据进行 CSRF 验证。诀窍是将 CSRF 令牌添加到要提交的表单数据中。有关示例 HTML,请参见随附的电子书代码。

这是我们的视图支持POSTPUTREMOVE方法:

@app.route("/articles/", methods=["GET", "POST"])
@app.route("/articles/<int:article_id>", methods=["GET", "PUT", "DELETE"])
def articles(article_id=None):
    if request.method == "GET":
        if article_id:
            article = Article.query.get(article_id)

            if request.is_xhr:
                if article is None:
                    return jsonify({"msgs": ["the article you're looking for could not be found"]}), 404

                result = article_schema.dump(article)
                return jsonify({': result.data})

            return render_template('article.html', article=article, form=ArticleForm(obj=article))
        else:
            if request.is_xhr:
                # never return the whole set! As it would be very slow
                queryset = Article.query.limit(10)
                result = articles_schema.dump(queryset)

                # jsonify serializes our dict into a proper flask response
                return jsonify({"articles": result.data})
    elif request.method == "POST" and request.is_xhr:
        form = ArticleForm(request.form)

        if form.validate():
            article = Article()
            form.populate_obj(article)
            db.session.add(article)
            db.session.commit()
            return jsonify({"msgs": ["article created"]})
        else:
            return jsonify({"msgs": ["the sent data is not valid"]}), 400

    elif request.method == "PUT" and request.is_xhr:
        article = Article.query.get(article_id)

        if article is None:
            return jsonify({"msgs": ["the article you're looking for could not be found"]}), 404

        form = ArticleForm(request.form, obj=article)

        if form.validate():
            form.populate_obj(article)
            db.session.add(article)
            db.session.commit()
            return jsonify({"msgs": ["article updated"]})
        else:
            return jsonify({"msgs": ["the sent data was not valid"]}), 400
    elif request.method == "DELETE" and request.is_xhr:
        article = Article.query.get(article_id)

        if article is None:
            return jsonify({"msgs": ["the article you're looking for could not be found"]}), 404

        db.session.delete(article)
        db.session.commit()
        return jsonify({"msgs": ["article removed"]})

    return render_template('articles.html', form=ArticleForm())

好吧,事实就是这样,我们再也不能隐藏了;在同一页中处理 Web 服务和纯 HTML 渲染可能有点混乱,就像前面的例子所示。即使您将函数按方法分割到其他函数中,事情可能看起来也不那么好。通常的模式是有一个视图用于处理 Ajax 请求,另一个用于处理“正常”请求。只有在方便的情况下才会混合使用两者。

Flask-Restless

Flask-Restless 是一个扩展,能够自动生成整个 RESTful API,支持GETPOSTPUTDELETE,用于你的 SQLAlchemy 模型。大多数 Web 服务不需要更多。使用 Flask-Restless 的另一个优势是可以扩展自动生成的方法,进行身份验证验证、自定义行为和自定义查询。这是一个必学的扩展!

让我们看看我们的 Web 服务在 Flask-Restless 下会是什么样子。我们还需要为这个示例安装一个新的库:

pip install Flask-Restless

然后:

# coding:utf-8

from flask import Flask, url_for
from flask.ext.restless import APIManager
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/employees.sqlite'

db = SQLAlchemy(app)

class Article(db.Model):
    __tablename__ = 'articles'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.String(255), nullable=False)

    def __unicode__(self):
        return self.content

    def url(self):
        return url_for('.articles', article_id=self.id)

# create the Flask-Restless API manager
manager = APIManager(app, flask_sqlalchemy_db=db)

# create our Article API at /api/articles
manager.create_api(Article, collection_name='articles', methods=['GET', 'POST', 'PUT', 'DELETE'])

db.create_all()

if __name__ == '__main__':
    # we define the debug environment only if running through command line
    app.config['SQLALCHEMY_ECHO'] = True
    app.debug = True
    app.run()

在前面的示例中,我们创建了我们的模型,然后创建了一个 Flask-Restless API 来保存所有我们的模型 API;然后我们为Article创建了一个带有前缀articles的 Web 服务 API,并支持GETPOSTPUTDELETE方法,每个方法都有预期的行为:GET用于查询,POST用于新记录,PUT用于更新,DELETE用于删除。

在控制台中,输入以下命令发送 GET 请求到 API,并测试您的示例是否正常工作:

curl http://127.0.0.1:5000/api/articles

由于 Flask-Restless API 非常广泛,我们将简要讨论一些对大多数项目非常有用的常见选项。

create_apiserializer/deserializer参数在您需要为模型进行自定义序列化/反序列化时非常有用。使用方法很简单:

manager.create_api(Model, methods=METHODS,
                   serializer=my_serializer,
                   deserializer=my_deserializer)
def my_serializer(instance):
    return some_schema.dump(instance).data

def my_deserializer(data):
    return some_schema.load(data).data

您可以使用 marshmallow 生成模式,就像前面的示例一样。

create_api的另一个有用的选项是include_columnsexclude_columns。它们允许您控制 API 返回多少数据,并防止返回敏感数据。当设置include_columns时,只有其中定义的字段才会被 GET 请求返回。当设置exclude_columns时,只有其中未定义的字段才会被 GET 请求返回。例如:

# both the statements below are equivalents
manager.create_api(Article, methods=['GET'], include_columns=['id', 'title'])
manager.create_api(Article, methods=['GET'], exclude_columns=['content'])

总结

在本章中,我们学习了 REST 是什么,它的优势,如何创建 Flask RESTful Web 服务和 API,以及如何使用 Flask-Restless 使整个过程顺利运行。我们还概述了 jQuery 是什么,以及如何使用它发送 Ajax 请求来查询我们的服务。这些章节示例非常深入。尝试自己编写示例代码,以更好地吸收它们。

在下一章中,我们将讨论确保软件质量的一种方式:测试!我们将学习如何以各种方式测试我们的 Web 应用程序,以及如何将这些测试集成到我们的编码例程中。到时见!

第七章:如果没有经过测试,那就不是游戏,兄弟!

您编写的软件是否具有质量?您如何证明?

通常根据特定的需求编写软件,无论是错误报告、功能和增强票据,还是其他。为了具有质量,软件必须完全和准确地满足这些需求;也就是说,它应该做到符合预期。

就像您会按下按钮来了解它的功能一样(假设您没有手册),您必须测试您的代码以了解它的功能或证明它应该做什么。这就是您确保软件质量的方式。

在软件开发过程中,通常会有许多共享某些代码库或库的功能。例如,您可以更改一段代码以修复错误,并在代码的另一个点上创建另一个错误。软件测试也有助于解决这个问题,因为它们确保您的代码执行了应该执行的操作;如果您更改了一段错误的代码并且破坏了另一段代码,您也将破坏一个测试。在这种情况下,如果您使用持续集成,则破损的代码将永远不会到达生产环境。

提示

不知道什么是持续集成?请参考www.martinfowler.com/articles/continuousIntegration.htmljenkins-ci.org/

测试是如此重要,以至于有一个称为测试驱动开发TDD)的软件开发过程,它规定测试应该在实际代码之前编写,并且只有当测试本身得到满足时,实际代码才是准备就绪。TDD 在高级开发人员及以上中非常常见。就为了好玩,我们将在本章中从头到尾使用 TDD。

有哪些测试类型?

我们想要测试,我们现在就想要;但是我们想要什么样的测试呢?

测试有两种主要分类,根据你对内部代码的访问程度:黑盒白盒测试。

黑盒测试是指测试人员对其正在测试的实际代码没有知识和/或访问权限。在这些情况下,测试包括检查代码执行前后的系统状态是否符合预期,或者给定的输出是否对应于给定的输入。

白盒测试有所不同,因为您将可以访问您正在测试的实际代码内部,以及代码执行前后的系统预期状态和给定输入的预期输出。这种测试具有更强烈的主观目标,通常与性能和软件质量有关。

在本章中,我们将介绍如何实施黑盒测试,因为它们更容易让其他人接触并且更容易实施。另一方面,我们将概述执行白盒测试的工具。

代码库可能经过多种方式测试。我们将专注于两种类型的自动化测试(我们不会涵盖手动测试技术),每种测试都有不同的目标:单元测试行为测试。这些测试各自有不同的目的,并相互补充。让我们看看这些测试是什么,何时使用它们以及如何在 Flask 中运行它们。

单元测试

单元测试是一种技术,您可以针对具有有意义功能的最小代码片段(称为单元)对输入和预期输出进行测试。通常,您会对代码库中不依赖于您编写的其他函数和方法的函数和方法运行单元测试。

在某种意义上,测试实际上是将单元测试堆叠在一起的艺术(首先测试一个函数,然后相互交互的函数,然后与其他系统交互的函数),以便整个系统最终得到充分测试。

对于 Python 的单元测试,我们可以使用内置模块doctestunittestdoctest模块用于作为测试用例运行来自对象文档的嵌入式交互式代码示例。Doctests 是 Unittest 的一个很好的补充,Unittest 是一个更健壮的模块,专注于帮助您编写单元测试(正如其名称所暗示的那样),最好不要单独使用。让我们看一个例子:

# coding:utf-8

"""Doctest example"""

import doctest
import unittest

def sum_fnc(a, b):
    """
    Returns a + b

    >>> sum_fnc(10, 20)
    30
    >>> sum_fnc(-10, -20)
    -30
    >>> sum_fnc(10, -20)
    -10
    """
    return a + b

class TestSumFnc(unittest.TestCase):
    def test_sum_with_positive_numbers(self):
        result = sum_fnc(10, 20)
        self.assertEqual(result, 30)

    def test_sum_with_negative_numbers(self):
        result = sum_fnc(-10, -20)
        self.assertEqual(result, -30)

    def test_sum_with_mixed_signal_numbers(self):
        result = sum_fnc(10, -20)
        self.assertEqual(result, -10)

if __name__ == '__main__':
    doctest.testmod(verbose=1)
    unittest.main()

在前面的例子中,我们定义了一个简单的sum_fnc函数,它接收两个参数并返回它们的和。sum_fnc函数有一个解释自身的文档字符串。在这个文档字符串中,我们有一个函数调用和输出的交互式代码示例。这个代码示例是由doctest.testmod()调用的,它检查给定的输出是否对于调用的函数是正确的。

接下来,我们有一个名为TestSumFncTestCase,它定义了三个测试方法(test_<test_name>),并且几乎完全与我们的文档字符串测试相同。这种方法的不同之处在于,我们能够在没有测试结果的情况下发现问题,如果有问题。如果我们希望对我们的文档字符串和测试用例做完全相同的事情,我们将在测试方法中使用assert Python 关键字来将结果与预期结果进行比较。相反,我们使用了assertEqual方法,它不仅告诉我们如果结果有问题,还告诉我们问题是结果和预期值都不相等。

如果我们希望检查我们的结果是否大于某个值,我们将使用assertGreaterassertGreaterEqual方法,这样断言错误也会告诉我们我们有什么样的错误。

提示

良好的测试彼此独立,以便一个失败的测试永远不会阻止另一个测试的运行。从测试中导入测试依赖项并清理数据库是常见的做法。

在编写脚本或桌面应用程序时,前面的情况很常见。Web 应用程序对测试有不同的需求。Web 应用程序代码通常是响应通过浏览器请求的用户交互而运行,并返回响应作为输出。要在这种环境中进行测试,我们必须模拟请求并正确测试响应内容,这通常不像我们的sum_fnc的输出那样直截了当。响应可以是任何类型的文档,它可能具有不同的大小和内容,甚至您还必须担心响应的 HTTP 代码,这包含了很多上下文含义。

为了帮助您测试视图并模拟用户与您的 Web 应用程序的交互,Flask 为您提供了一个测试客户端工具,通过它您可以向您的应用程序发送任何有效的 HTTP 方法的请求。例如,您可以通过PUT请求查询服务,或者通过GET请求查看常规视图。这是一个例子:

# coding:utf-8

from flask import Flask, url_for, request
import unittest

def setup_database(app):
    # setup database ...
    pass

def setup(app):
    from flask import request, render_template

    # this is not a good production setup
    # you should register blueprints here
    @app.route("/")
    def index_view():
        return render_template('index.html', name=request.args.get('name'))

def app_factory(name=__name__, debug=True):
    app = Flask(name)
    app.debug = debug
    setup_database(app)
    setup(app)
    return app

class TestWebApp(unittest.TestCase):
    def setUp(self):
        # setUp is called before each test method
        # we create a clean app for each test
        self.app = app_factory()
        # we create a clean client for each test
        self.client = self.app.test_client()

    def tearDown(self):
        # release resources here
        # usually, you clean or destroy the test database
        pass

    def test_index_no_arguments(self):
        with self.app.test_request_context():
            path = url_for('index_view')
            resp = self.client.get(path)
            # check response content
            self.assertIn('Hello World', resp.data)

    def test_index_with_name(self):
        with self.app.test_request_context():
            name = 'Amazing You'
            path = url_for('index_view', name=name)
            resp = self.client.get(path)
            # check response content
            self.assertIn(name, resp.data)

if __name__ == '__main__':
    unittest.main()

前面的例子是一个完整的例子。我们使用app_factory模式来创建我们的应用程序,然后我们在setUp中创建一个应用程序和客户端,这在每个测试方法运行之前运行,我们创建了两个测试,一个是当请求接收到一个名称参数时,另一个是当请求没有接收到名称参数时。由于我们没有创建任何持久资源,我们的tearDown方法是空的。如果我们有任何类型的数据库连接和固定装置,我们将不得不在tearDown中重置数据库状态,甚至删除数据库。

此外,要注意test_request_context,它用于在我们的测试中创建一个请求上下文。我们创建这个上下文,以便url_for能够返回我们的视图路径,如果没有设置SERVER_NAME配置,它需要一个请求上下文。

提示

如果您的网站使用子域,设置SERVER_NAME配置。

行为测试

在单元测试中,我们测试函数的输出与预期结果。如果结果不是我们等待的结果,将引发断言异常以通知问题。这是一个简单的黑盒测试。现在,一些奇怪的问题:您是否注意到您的测试是以与错误报告或功能请求不同的方式编写的?您是否注意到您的测试不能被非技术人员阅读,因为它实际上是代码?

我想向您介绍 lettuce(lettuce.it/),这是一个能够将Gherkin语言测试转换为实际测试的工具。

提示

有关 Gherkin 语言的概述,请访问github.com/cucumber/cucumber/wiki/Gherkin

Lettuce 可以帮助您将实际用户编写的功能转换为测试方法调用。这样,一个功能请求就像:

功能:计算总和

为了计算总和

作为学生

实现sum_fnc

  • 场景:正数之和

  • 假设我有数字 10 和 20

  • 我把它们加起来

  • 然后我看到结果 30

  • 场景:负数之和

  • 假设我有数字-10 和-20

  • 我把它们加起来

  • 然后我看到结果-30

  • 场景:混合信号之和

  • 假设我有数字 10 和-20

  • 我把它们加起来

  • 然后我看到结果-10

该功能可以转换为将测试软件的实际代码。确保 lettuce 已正确安装:

pip install lettuce python-Levenshtein

创建一个features目录,并在其中放置一个steps.py(或者您喜欢的任何其他 Python 文件名),其中包含以下代码:

# coding:utf-8
from lettuce import *
from lib import sum_fnc

@step('Given I have the numbers (\-?\d+) and (\-?\d+)')
def have_the_numbers(step, *numbers):
    numbers = map(lambda n: int(n), numbers)
    world.numbers = numbers

@step('When I sum them')
def compute_sum(step):
    world.result = sum_fnc(*world.numbers)

@step('Then I see the result (\-?\d+)')
def check_number(step, expected):
    expected = int(expected)
    assert world.result == expected, "Got %d; expected %d" % (world.result, expected)

我们刚刚做了什么?我们定义了三个测试函数,have_the_numbers,compute_sum 和 check_number,其中每个函数都接收一个step实例作为第一个参数,以及用于实际测试的其他参数。用于装饰我们的函数的 step 装饰器用于将从我们的 Gherkin 文本解析的字符串模式映射到函数本身。我们的装饰器的另一个职责是将从步骤参数映射到函数的参数的参数解析为参数。

例如,have_the_numbers的步骤具有正则表达式模式(\-?\d+)和(\-?\d+),它将两个数字映射到我们函数的numbers参数。这些值是从我们的 Gherkin 输入文本中获取的。对于给定的场景,这些数字分别是[10, 20],[-10, -20]和[10, -20]。最后,world是一个全局变量,您可以在步骤之间共享值。

使用功能描述行为对开发过程非常有益,因为它使业务人员更接近正在创建的内容,尽管它相当冗长。另外,由于它冗长,不建议在测试孤立的函数时使用,就像我们在前面的例子中所做的那样。行为应该由业务人员编写,也应该测试编写人员可以直观证明的行为。例如,“如果我点击一个按钮,我会得到某物的最低价格”或“假设我访问某个页面,我会看到一些消息或一些链接”。

“点击这里,然后那里发生了什么”。检查渲染的请求响应有点棘手,如果您问我的话。为什么?在我们的第二个例子中,我们验证了给定的字符串值是否在我们的resp.data中,这是可以的,因为我们的响应返回complete。我们不使用 JavaScript 在页面加载后渲染任何内容或显示消息。如果是这种情况,我们的验证可能会返回错误的结果,因为 JavaScript 代码不会被执行。

为了正确呈现和验证view响应,我们可以使用无头浏览器,如SeleniumPhantomJS(参见pythonhosted.org/Flask-Testing/#testing-with-liveserver)。Flask-testing扩展也会有所帮助。

Flask-testing

与大多数 Flask 扩展一样,Flask-testing 并没有做太多事情,但它所做的事情都做得很好!我们将讨论 Flask-testing 提供的一些非常有用的功能:LiveServer 设置,额外的断言和 JSON 响应处理。在继续之前,请确保已安装:

pip install flask-testing blinker

LiveServer

LiveServer 是一个 Flask-testing 工具,允许您连接到无头浏览器,即不会将内容可视化呈现的浏览器(如 Firefox 或 Chrome),但会执行所有脚本和样式,并模拟用户交互。每当您需要在 JavaScript 交互后评估页面内容时,请使用 LiveServer。我们将使用 PhantomJS 作为我们的无头浏览器。我给您的建议是,您像我们的祖先一样安装旧浏览器,从源代码编译它。请按照phantomjs.org/build.html上的说明进行操作(您可能需要安装一些额外的库以获得 phantom 的全部功能)。build.sh文件将在必要时建议您安装它。

提示

编译PhantomJS后,确保它在您的 PATH 中被找到,将二进制文件bin/phantomjs移动到/usr/local/bin

确保安装了 Selenium:

pip install selenium

我们的代码将如下所示:

# coding:utf-8

"""
Example adapted from https://pythonhosted.org/Flask-Testing/#testing-with-liveserver
"""

import urllib2
from urlparse import urljoin
from selenium import webdriver
from flask import Flask, render_template, jsonify, url_for
from flask.ext.testing import LiveServerTestCase
from random import choice

my_lines = ['Hello there!', 'How do you do?', 'Flask is great, ain't it?']

def setup(app):
    @app.route("/")
    def index_view():
        return render_template('js_index.html')

    @app.route("/text")
    def text_view():
        return jsonify({'text': choice(my_lines)})

def app_factory(name=None):
    name = name or __name__
    app = Flask(name)
    setup(app)
    return app

class IndexTest(LiveServerTestCase):
    def setUp(self):
        self.driver = webdriver.PhantomJS()

    def tearDown(self):
        self.driver.close()

    def create_app(self):
        app = app_factory()
        app.config['TESTING'] = True
        # default port is 5000
        app.config['LIVESERVER_PORT'] = 8943
        return app

    def test_server_is_up_and_running(self):
        resp = urllib2.urlopen(self.get_server_url())
        self.assertEqual(resp.code, 200)

    def test_random_text_was_loaded(self):
        with self.app.test_request_context():
            domain = self.get_server_url()
            path = url_for('.index_view')
            url = urljoin(domain, path)

            self.driver.get(url)
            fillme_element = self.driver.find_element_by_id('fillme')
            fillme_text = fillme_element.text
            self.assertIn(fillme_text, my_lines)

if __name__ == '__main__':
    import unittest
    unittest.main()

templates/js_index.html文件应如下所示:

<html>
<head><title>Hello You</title></head>
<body>
<span id="fillme"></span>

<!-- Loading JQuery from CDN -->
<!-- what's a CDN? http://www.rackspace.com/knowledge_center/article/what-is-a-cdn -->
<script type="text/javascript" src="img/jquery-2.1.3.min.js"></script>
<script type="text/javascript">
  $(document).ready(function(){
    $.getJSON("{{ url_for('.text_view') }}",
    function(data){
       $('#fillme').text(data['text']);
    });
  });
</script>
</body></html>

前面的例子非常简单。我们定义了我们的工厂,它创建了我们的应用程序并附加了两个视图。一个返回一个带有脚本的js_index.html,该脚本查询我们的第二个视图以获取短语,并填充fillme HTML 元素,第二个视图以 JSON 格式返回一个从预定义列表中随机选择的短语。

然后我们定义IndexTest,它扩展了LiveServerTestCase,这是一个特殊的类,我们用它来运行我们的实时服务器测试。我们将我们的实时服务器设置为在不同的端口上运行,但这并不是必需的。

setUp中,我们使用 selenium WebDriver 创建一个driver。该驱动程序类似于浏览器。我们将使用它通过 LiveServer 访问和检查我们的应用程序。tearDown确保每次测试后关闭我们的驱动程序并释放资源。

test_server_is_up_and_running是不言自明的,在现实世界的测试中实际上是不必要的。

然后我们有test_random_text_was_loaded,这是一个非常繁忙的测试。我们使用test_request_context来创建一个请求上下文,以便使用url_open.get_server_url生成我们的 URL 路径,这将返回我们的实时服务器 URL;我们将这个 URL 与我们的视图路径连接起来并加载到我们的驱动程序中。

使用加载的 URL(请注意,URL 不仅加载了,而且脚本也执行了),我们使用find_element_by_id来查找元素fillme并断言其文本内容具有预期值之一。这是一个简单的例子。例如,您可以测试按钮是否在预期位置;提交表单;并触发 JavaScript 函数。Selenium 加上 PhantomJS 是一个强大的组合。

提示

当您的开发是由功能测试驱动时,您实际上并没有使用TDD,而是行为驱动开发BDD)。通常,两种技术的混合是您想要的。

额外的断言

在测试代码时,您会注意到一些测试有点重复。为了处理这种情况,可以创建一个具有特定例程的自定义 TestCases,并相应地扩展测试。使用 Flask-testing,您仍然需要这样做,但是要编写更少的代码来测试您的 Flask 视图,因为flask.ext.testing.TestCase捆绑了常见的断言,许多在 Django 等框架中找到。让我们看看最重要的(在我看来,当然)断言:

  • assert_context(name, value): 这断言一个变量是否在模板上下文中。用它来验证给定的响应上下文对于一个变量具有正确的值。

  • assert_redirects(response, location): 这断言了响应是一个重定向,并给出了它的位置。在写入存储后进行重定向是一个很好的做法,比如在成功的 POST 后,这是这个断言的一个很好的使用案例。

  • assert_template_used(name, tmpl_name_attribute='name'):这断言了请求中使用了给定的模板(如果您没有使用 Jinja2,则需要 tmpl_name_attribute;在我们的情况下不需要);无论何时您渲染一个 HTML 模板,都可以使用它!

  • assert404(response, message=None): 这断言了响应具有 404 HTTP 状态码;它对于“雨天”场景非常有用;也就是说,当有人试图访问不存在的内容时。它非常有用。

JSON 处理

Flask-testing 为您提供了一个可爱的技巧。每当您从视图返回一个 JSON 响应时,您的响应将有一个额外的属性叫做 json。那就是您的 JSON 转换后的响应!以下是一个例子:

# example from https://pythonhosted.org/Flask-Testing/#testing-json-responses
@app.route("/ajax/")
def some_json():
    return jsonify(success=True)

class TestViews(TestCase):
    def test_some_json(self):
        response = self.client.get("/ajax/")
        self.assertEquals(response.json, dict(success=True))

固定装置

良好的测试总是在考虑预定义的、可重现的应用程序状态下执行;也就是说,无论何时您在选择的状态下运行测试,结果都将是等价的。通常,这是通过自己设置数据库数据并清除缓存和任何临时文件(如果您使用外部服务,您应该模拟它们)来实现的。清除缓存和临时文件并不难,而设置数据库数据则不然。

如果您使用 Flask-SQLAlchemy 来保存您的数据,您需要在您的测试中某个地方硬编码如下:

attributes = { … }
model = MyModel(**attributes)
db.session.add(model)
db.session.commit()

这种方法不易扩展,因为它不容易重复使用(当您将其定义为一个函数和一个方法时,为每个测试定义它)。有两种方法可以为测试填充您的数据库:固定装置伪随机数据

使用伪随机数据通常是特定于库的,并且生成的数据是上下文特定的,而不是静态的,但有时可能需要特定的编码,就像当您定义自己的字段或需要字段的不同值范围时一样。

固定装置是最直接的方法,因为您只需在文件中定义您的数据,并在每个测试中加载它。您可以通过导出数据库数据,根据您的方便进行编辑,或者自己编写。JSON 格式在这方面非常受欢迎。让我们看看如何实现这两种方法:

# coding:utf-8
# == USING FIXTURES ===
import tempfile, os
import json

from flask import Flask
from flask.ext.testing import TestCase
from flask.ext.sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255))
    gender = db.Column(db.String(1), default='U')

    def __unicode__(self):
        return self.name

def app_factory(name=None):
    name = name or __name__
    app = Flask(name)
    return app

class MyTestCase(TestCase):
    def create_app(self):
        app = app_factory()
        app.config['TESTING'] = True
        # db_fd: database file descriptor
        # we create a temporary file to hold our data
        self.db_fd, app.config['DATABASE'] = tempfile.mkstemp()
        db.init_app(app)
        return app

    def load_fixture(self, path, model_cls):
        """
        Loads a json fixture into the database
        """
        fixture = json.load(open(path))

        for data in fixture:
            # Model accepts dict like parameter
            instance = model_cls(**data)
            # makes sure our session knows about our new instance
            db.session.add(instance)

        db.session.commit()

    def setUp(self):
        db.create_all()
        # you could load more fixtures if needed
        self.load_fixture('fixtures/users.json', User)

    def tearDown(self):
        # makes sure the session is removed
        db.session.remove()

        # close file descriptor
        os.close(self.db_fd)

        # delete temporary database file
        # as SQLite database is a single file, this is equivalent to a drop_all
        os.unlink(self.app.config['DATABASE'])

    def test_fixture(self):
        marie = User.query.filter(User.name.ilike('Marie%')).first()
        self.assertEqual(marie.gender, "F")

if __name__ == '__main__':
    import unittest
    unittest.main()

上述代码很简单。我们创建一个 SQLAlchemy 模型,将其链接到我们的应用程序,并在设置期间加载我们的固定装置。在 tearDown 中,我们确保我们的数据库和 SQLAlchemy 会话对于下一个测试来说是全新的。我们的固定装置是使用 JSON 格式编写的,因为它足够快速且可读。

如果我们使用伪随机生成器来创建我们的用户(查找 Google 上关于这个主题的 模糊测试),我们可以这样做:

def new_user(**kw):
    # this way we only know the user data in execution time
    # tests should consider it
    kw['name'] = kw.get('name', "%s %s" % (choice(names), choice(surnames)) )
    kw['gender'] = kw.get('gender', choice(['M', 'F', 'U']))
    return kw
user = User(**new_user())
db.session.add(user)
db.session.commit()

请注意,由于我们不是针对静态场景进行测试,我们的测试也会发生变化。通常情况下,固定装置在大多数情况下就足够了,但伪随机测试数据在大多数情况下更好,因为它迫使您的应用处理真实场景,而这些通常被忽略。

额外 - 集成测试

集成测试是一个非常常用的术语/概念,但其含义非常狭窄。它用于指代测试多个模块一起测试它们的集成。由于使用 Python 从同一代码库中测试多个模块通常是微不足道且透明的(这里导入,那里调用,以及一些输出检查),您通常会听到人们在指代测试他们的代码与不同代码库进行集成测试时使用术语 集成测试,或者当系统添加了新的关键功能时。

总结

哇!我们刚刚度过了一章关于软件测试的内容!这是令人自豪的成就。我们学到了一些概念,比如 TDD、白盒测试和黑盒测试。我们还学会了如何创建单元测试;测试我们的视图;使用 Gherkin 语言编写功能并使用 lettuce 进行测试;使用 Flask-testing、Selenium 和 PhantomJS 来测试用户角度的 HTML 响应;还学会了如何使用固定装置来控制我们应用程序的状态,以进行正确可重复的测试。现在,您可以使用不同的技术以正确的方式测试 Flask 应用程序,以满足不同的场景和需求。

在下一章中,事情会变得非常疯狂,因为我们的研究对象将是使用 Flask 的技巧。下一章将涵盖蓝图、会话、日志记录、调试等内容,让您能够创建更加健壮的软件。到时见!

第八章:Flask 的技巧或巫术 101

在尝试更高级的 Flask 主题之前,你还能等多久?我肯定不能!在本章中,我们将学习技术和模块,这些对于更好更高效地使用 Flask 至关重要。

高质量的软件需要花费很长时间编码,或者低质量的软件可以很快交付?真正的 Web 开发,也就是你在月底拿到薪水的那种,需要可维护性,生产力和质量才能成为可能。

正如我们之前讨论的,软件质量与测试密切相关。衡量软件质量的一种方法是验证其功能与预期功能的接近程度。这种衡量并不考虑质量评估的主观方面。例如,客户可能认为他最新项目的设计很丑,认为一个经过良好测试的,符合功能的 Web 项目是糟糕的。在这些情况下,你所能做的就是为设计重构收取一些额外的费用。

提示

如果你遇到这种情况,可以让你的客户更接近开发过程,以避免这种情况。尝试在 Google 或 DuckDuckGo 中搜索“scrum”。

在谈论生产力可维护性时,方法有很多!你可以购买一个像 PyCharm 或 WingIDE 这样的好的集成开发环境(IDE)来提高你的生产力,或者雇佣第三方服务来帮助你测试你的代码或控制你的开发进度,但这些只能做到这么多。良好的架构和任务自动化将是大多数项目中的最好朋友。在讨论如何组织你的代码以及哪些模块将帮助你节省一些打字之前,让我们讨论一下过早优化和过度设计,这是焦虑的开发人员/分析师/好奇的经理的两个可怕的症状。

过度设计

制作软件有点像制作公寓,有一些相似之处。在开始之前,你会提前计划你想要创造的东西,以便将浪费降到最低。与公寓相反,你不必计划你的软件,因为它在开发过程中很可能会发生变化,而且很多计划可能只是浪费。

这种“计划刚刚好”的方法的问题在于你不知道未来会发生什么,这可能会将我们内心的一点点偏执变成一些大问题。一个人可能最终会编写针对完全系统故障或复杂软件需求场景的代码,而这些可能永远不会发生。你不需要多层架构,缓存,数据库集成,信号系统等等,来创建一个 hello world,也不需要少于这些来创建一个 Facebook 克隆。

这里的信息是:不要使你的产品比你知道它需要的更健壮或更复杂,也不要浪费时间计划可能永远不会发生的事情。

提示

始终计划合理的安全性,复杂性和性能水平。

过早优化

你的软件足够快吗?不知道?那么为什么要优化代码,我的朋友?当你花时间优化你不确定是否需要优化的软件时,如果没有人抱怨它运行缓慢,或者你在日常使用中没有注意到它运行缓慢,你可能正在浪费时间进行过早优化。

所以,开始 Flask 吧。

蓝图 101

到目前为止,我们的应用程序都是平面的:美丽的,单文件的 Web 应用程序(不考虑模板和静态资源)。在某些情况下,这是一个不错的方法;减少了对导入的需求,易于使用简单的编辑器进行维护,但是...

随着我们的应用程序的增长,我们发现需要上下文地安排我们的代码。Flask 蓝图允许你将项目模块化,将你的视图分片成“类似应用程序”的对象,称为蓝图,这些蓝图可以稍后由你的 Flask 应用程序加载和公开。大型应用程序受益于使用蓝图,因为代码变得更有组织性。

在功能上,它还可以帮助您以更集中的方式配置已注册的视图访问和资源查找。测试、模型、模板和静态资源可以按蓝图进行排序,使您的代码更易于维护。如果您熟悉Django,可以将蓝图视为 Django 应用程序。这样,注册的蓝图可以访问应用程序配置,并可以使用不同的路由进行注册。

与 Django 应用程序不同,蓝图不强制执行特定的结构,就像 Flask 应用程序本身一样。例如,您可以将蓝图结构化为模块,这在某种程度上是方便的。

例子总是有帮助的,对吧?让我们看一个蓝图的好例子。首先,在我们的虚拟环境中安装了示例所需的库:

# library for parsing and reading our HTML
pip install lxml
# our test-friendly library
pip install flask-testing

然后我们定义了我们的测试(因为我们喜欢 TDD!):

# coding:utf-8
# runtests.py

import lxml.html

from flask.ext.testing import TestCase
from flask import url_for
from main import app_factory
from database import db

class BaseTest(object):
    """
    Base test case. Our test cases should extend this class.
    It handles database creation and clean up.
    """

    def create_app(self):
        app = app_factory()
        app.config['TESTING'] = True
        return app

    def setUp(self):
        self.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/ex01_test.sqlite'
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()

class PostDetailTest(BaseTest, TestCase):
    def add_single_post(self):
        from blog import Post

        db.session.add(Post(title='Some text', slug='some-text', content='some content'))
        db.session.commit()

        assert Post.query.count() == 1

    def setUp(self):
        super(PostDetailTest, self).setUp()
        self.add_single_post()

    def test_get_request(self):
        with self.app.test_request_context():
            url = url_for('blog.posts_view', slug='some-text')
            resp = self.client.get(url)
            self.assert200(resp)
            self.assertTemplateUsed('post.html')
            self.assertIn('Some text', resp.data)

class PostListTest(BaseTest, TestCase):
    def add_posts(self):
        from blog import Post

        db.session.add_all([
            Post(title='Some text', slug='some-text', content='some content'),
            Post(title='Some more text', slug='some-more-text', content='some more content'),
            Post(title='Here we go', slug='here-we-go', content='here we go!'),
        ])
        db.session.commit()

        assert Post.query.count() == 3

    def add_multiple_posts(self, count):
        from blog import Post

        db.session.add_all([
            Post(title='%d' % i, slug='%d' % i, content='content %d' % i) for i in range(count)
        ])
        db.session.commit()

        assert Post.query.count() == count

    def test_get_posts(self):
        self.add_posts()

        # as we want to use url_for ...
        with self.app.test_request_context():
            url = url_for('blog.posts_view')
            resp = self.client.get(url)

            self.assert200(resp)
            self.assertIn('Some text', resp.data)
            self.assertIn('Some more text', resp.data)
            self.assertIn('Here we go', resp.data)
            self.assertTemplateUsed('posts.html')

    def test_page_number(self):
        self.add_multiple_posts(15)

        with self.app.test_request_context():
            url = url_for('blog.posts_view')
            resp = self.client.get(url)

            self.assert200(resp)

            # we use lxml to count how many li results were returned
            handle = lxml.html.fromstring(resp.data)
            self.assertEqual(10, len(handle.xpath("//ul/li")))

if __name__ == '__main__':
    import unittest
    unittest.main()

在前面的代码中,我们测试了一个单个视图blog.posts_view,它有两个路由,一个用于帖子详细信息,另一个用于帖子列表。如果我们的视图接收到一个slug参数,它应该只返回具有 slug 属性值的第一个Post;如果没有,它将返回最多 10 个结果。

现在我们可以创建一个视图,使用满足我们测试的蓝图:

# coding:utf-8
# blog.py

from flask import Blueprint, render_template, request
from database import db

# app is usually a good name for your blueprint instance
app = Blueprint(
    'blog',  # our blueprint name and endpoint prefix
    # template_folder points out to a templates folder in the current module directory
    __name__, template_folder='templates'
)

class Post(db.Model):
    __tablename__ = 'posts'

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    slug = db.Column(db.String(100), nullable=False, unique=True)
    content = db.Column(db.Text(), nullable=False)

    def __unicode__(self):
        return self.title

@app.route("/")
@app.route("/<slug>")
def posts_view(slug=None):
    if slug is not None:
        post = Post.query.filter_by(slug=slug).first()
        return render_template('post.html', post=post)

    # lets paginate our result
    page_number = into(request.args.get('page', 1))
    page = Post.query.paginate(page_number, 10)

    return render_template('posts.html', page=page)

创建蓝图非常简单:我们提供蓝图名称,该名称也用作所有蓝图视图的端点前缀,导入名称(通常为__name__),以及我们认为合适的任何额外参数。在示例中,我们传递了template_folder作为参数,因为我们想使用模板。如果您正在编写服务,可以跳过此参数。另一个非常有用的参数是url_prefix,它允许我们为所有路径定义默认的 URL 前缀。

提示

如果我们的蓝图名称是blog,并且我们注册了一个方法index_view,我们对该视图的端点将是blog.index_view。端点是对视图的“名称引用”,您可以将其转换为其 URL 路径。

下一步是在我们的 Flask 应用程序中注册我们的蓝图,以便使我们编写的视图可访问。还创建了一个database.py模块来保存我们的 db 实例。

请注意,我们的 Post 模型将被db.create_all识别,因为它是在blog.py中定义的;因此,当模块被导入时,它变得可见。

提示

如果您在任何地方导入了一个模块中定义的模型类,那么它的表可能不会被创建,因为 SQLAlchemy 将不知道它。避免这种情况的一种方法是让所有模型都由定义蓝图的模块导入。

# coding:utf-8
# database.py
from flask.ext.sqlalchemy import SQLAlchemy

db = SQLAlchemy()
## database.py END

# coding:utf-8
# main.py
from flask import Flask
from database import db
from blog import app as blog_bp

def app_factory(name=None):
    app = Flask(name or __name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/ex01.db'

    db.init_app(app)

    # let Flask know about blog blueprint
    app.register_blueprint(blog_bp)
    return app

# running or importing?
if __name__ == '__main__':
    app = app_factory()
    app.debug = True

    # make sure our tables are created
    with app.test_request_context():
        db.create_all()

    app.run()

我们在这里有什么?一个app_factory,它创建我们的 Flask 应用程序,在/tmp/中设置默认数据库,这是一个常见的 Linux 临时文件夹;初始化我们的数据库管理器,在database.py中定义;并使用register_blueprint注册我们的蓝图。

我们设置了一个例行程序来验证我们是在运行还是导入给定的模块(对于runtests.py很有用,因为它从main.py导入);如果我们正在运行它,我们创建一个应用程序,将其设置为调试模式(因为我们正在开发),在临时测试上下文中创建数据库(create_all不会在上下文之外运行),并运行应用程序。

模板(post.htmlposts.html)仍然需要编写。您能写出来使测试通过吗?我把它留给你来做!

我们当前的示例项目结构应该如下所示:

蓝图 101

嗯,我们的项目仍然是平的;所有模块都在同一级别上,上下文排列,但是平的。让我们尝试将我们的博客蓝图移动到自己的模块中!我们可能想要这样的东西:

蓝图 101

博客模板位于博客包中的模板文件夹中,我们的模型位于models.py中,我们的视图位于views.py中(就像 Django 应用程序一样,对吧?)。

可以轻松进行这种更改。主要是创建一个blog文件夹,并在其中放置一个带有以下内容的__init__.py文件:

# coding:utf-8
from views import *

Post类定义和 db 导入移到models.py中,并将特定于博客的模板post.htmlposts.html移到包内的templates文件夹中。由于template_folder是相对于当前模块目录的,因此无需更改我们的蓝图实例化。现在,运行您的测试。它们应该可以正常工作,无需修改。

喝一口水,戴上你的战斗头盔,让我们继续下一个话题:记录!

哦,天啊,请告诉我你有日志…

在面对一个你无法完全理解的神秘问题之前,你永远不会知道记录有多么重要。了解为什么会出现问题是人们将记录添加到他们的项目中的第一个,也可能是主要的原因。但是,嘿,什么是记录?

记录是存储有关事件的记录以供以后进一步分析的行为。关于记录的一个重要概念与记录级别有关,它允许您对信息类型和相关性进行分类。

Python 标准库捆绑了一个记录库,实际上非常强大,通过处理程序和消息,可以记录到流、文件、电子邮件或您认为合适的任何其他解决方案。让我们尝试一些有用的记录示例,好吗?

# coding:utf-8
from flask import Flask
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

# default flask logging handler pushes messages into the console
# works DEBUG mode only
app.config['LOG_FILENAME'] = '/var/tmp/project_name.log'
# log warning messages or higher
app.config['LOG_LEVEL'] = logging.WARNING
app.config['ADMINS'] = ['you@domain.com']
app.config['ENV'] = 'production'

def configure_file_logger(app, filename, level=logging.DEBUG):
    # special file handler that overwrites logging file after
    file_handler = RotatingFileHandler(
        filename=filename,
        encoding='utf-8',  # cool kids use utf-8
        maxBytes=1024 * 1024 * 32,  # we don't want super huge log files ...
        backupCount=3  # keep up to 3 old log files before rolling over
    )

    # define how our log messages should look like
    formatter = logging.Formatter(u"%(asctime)s %(levelname)s\t: %(message)s")
    file_handler.setFormatter(formatter)
    file_handler.setLevel(level)

    app.logger.addHandler(file_handler)

def configure_mail_logger(app, level=logging.ERROR):
    """
    Notify admins by e-mail in case of error for immediate action
    based on from http://flask.pocoo.org/docs/0.10/errorhandling/#error-mails
    """

    if app.config['ENV'] == 'production':
        from logging.handlers import SMTPHandler

        mail_handler = SMTPHandler(
            '127.0.0.1',
            'server-error@domain.com',
            app.config['ADMINS'], 'YourApplication Failed')

        mail_handler.setLevel(level)
        app.logger.addHandler(mail_handler)

if __name__ == '__main__':
    app.debug = True
    configure_file_logger(app, '/var/tmp/project_name.dev.log')
    configure_mail_logger(app)
    app.run()

在我们的示例中,我们创建了两个常见的记录设置:记录到文件和记录到邮件。它们各自的方式非常有用。在configure_file_logger中,我们定义了一个函数,将一个RotatingFileHandler注册到其中,以保存所有具有给定级别或以上的日志消息。在这里,我们不使用常规的FileHandler类,因为我们希望保持我们的日志文件可管理(也就是:小)。RotatingFileHandler允许我们为我们的日志文件定义一个最大大小,当日志文件大小接近maxBytes限制时,处理程序会“旋转”到一个全新的日志文件(或覆盖旧文件)。

记录到文件中非常简单,主要用于跟踪应用程序中的执行流程(主要是 INFO、DEBUG 和 WARN 日志)。基本上,文件记录应该在您有应该记录但不应立即阅读甚至根本不阅读的消息时使用(如果发生意外情况,您可能希望阅读 DEBUG 日志,但其他情况则不需要)。这样,在出现问题时,您只需查看日志文件,看看出了什么问题。邮件记录有另一个目标…

要配置我们的邮件记录器,我们定义一个名为configure_mail_logger的函数。它创建并注册一个SMTPHandler到我们的记录器在给定的记录级别;这样,每当记录一个具有该记录级别或更高级别的消息时,就会向注册的管理员发送一封电子邮件。

邮件记录有一个主要目的:尽快通知某人(或很多人)发生了重要事件,比如可能危及应用程序的错误。您可能不希望为此类处理程序设置低于 ERROR 的记录级别,因为会有太多的邮件需要跟踪。

关于记录的最后一条建议是,理智的项目都有良好的记录。追溯用户问题报告甚至邮件错误消息是很常见的。定义良好的记录策略并遵循它们,构建工具来分析您的日志,并设置适合项目需求的记录轮换参数。产生大量记录的项目可能需要更大的文件,而没有太多记录的项目可能可以使用较高值的backupCount。一定要仔细考虑一下。

调试、DebugToolbar 和幸福

在调试模式下运行 Flask 项目(app.debug = True)时,每当 Flask 检测到您的代码已更改,它将重新启动您的应用程序。如果给定的更改破坏了您的应用程序,Flask 将在控制台中显示一个非常简单的错误消息,可以很容易地分析。您可以从下往上阅读,直到找到第一行提到您编写的文件的行;这就是错误生成的地方。现在,从上往下阅读,直到找到一行告诉您确切的错误是什么。如果这种方法不够用,如果您需要读取变量值,例如更好地理解发生了什么,您可以使用pdb,标准的 Python 调试库,就像这样:

# coding:utf-8
from flask import Flask

app = Flask(__name__)

@app.route("/")
def index_view(arg=None):
    import pdb; pdb.set_trace()  # @TODO remove me before commit
    return 'Arg is %s' % arg

if __name__ == '__main__':
    app.debug = True
    app.run()

每当调用pdb.set_trace时,将打开一个pdb控制台,它非常像 Python 控制台。因此,您可以查询任何您需要的值,甚至进行代码评估。

使用pdb很好,但是,如果您只想了解您的请求发生了什么,例如使用的模板,CPU 时间(这可能会让您困惑!),已记录的消息等,Flask-DebugToolbar 可能是一个非常方便的扩展。

Flask-DebugToolbar

想象一下,您可以直接在渲染的模板中看到您的请求的 CPU 时间,并且可以验证使用哪个模板来渲染该页面,甚至可以实时编辑它。那会很好吗?您想看到它成真吗?那么请尝试以下示例:

首先,确保已安装扩展:

pip install flask-debugtoolbar

然后是一些精彩的代码:

# coding:utf-8
from flask import Flask, render_template
from flask_debugtoolbar import DebugToolbarExtension

app = Flask(__name__)
# configure your application before initializing any extensions
app.debug = True
app.config['SECRET_KEY'] = 'secret'  # required for session cookies to work
app.config['DEBUG_TB_TEMPLATE_EDITOR_ENABLED'] = True
toolbar = DebugToolbarExtension(app)

@app.route("/")
def index_view():
    # please, make sure templates/index.html exists ; )
    return render_template('index.html')

if __name__ == '__main__':
    app.run()

使用 Flask-DebugToolbar 没有什么神秘的。将debug设置为True,添加secret_key,并初始化扩展。当您在浏览器中打开http://127.0.0.1:5000/时,您应该看到类似这样的东西:

Flask-DebugToolbar

右侧的可折叠面板是调试工具栏在每个 HTML 响应中插入的一小部分 HTML,它允许您检查响应,而无需使用pdb等调试器。在示例中,我们将DEBUG_TB_TEMPLATE_EDITOR_ENABLED设置为True;此选项告诉 DebugToolbar 我们希望直接从浏览器中编辑渲染的模板。只需转到模板 | 编辑模板来尝试。

会话或在请求之间存储用户数据

有时,在应用程序中会出现这样的情况,需要在请求之间保留数据,但无需将其持久化在数据库中,比如用于标识已登录用户的身份验证令牌,或者用户添加到购物车中的商品。在这些危险时刻,请使用 Flask 会话。

Flask 会话是使用浏览器 cookie 和加密实现的请求之间的瞬时存储解决方案。Flask 使用秘钥值来加密您在会话中设置的任何值,然后将其设置在 cookie 中;这样,即使恶意人士可以访问受害者的浏览器,也无法读取 cookie 的内容。

提示

由于秘钥用于加密会话数据,因此为您的秘钥设置一个强大的值非常重要。os.urandom(24)可能会为部署环境创建一个强大的秘钥。

会话中存储的数据是瞬时的,因为不能保证它在任何时候都会存在,因为用户可能会清除浏览器的 cookie,或者 cookie 可能会过期,但如果您设置了它,它很可能会存在。在开发时,始终考虑这一点。

Flask 会话的一个重大优势是其简单性;您可以像使用常规字典一样使用它,就像这样:

# coding:utf-8

from flask import Flask, render_template, session, flash
from flask.ext.sqlalchemy import SQLAlchemy

app = Flask(__name__)
# strong secret key!!
app.config['SECRET_KEY'] = '\xa6\xb5\x0e\x7f\xd3}\x0b-\xaa\x03\x03\x82\x10\xbe\x1e0u\x93,{\xd4Z\xa3\x8f'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/ex05.sqlite'
db = SQLAlchemy(app)

class Product(db.Model):
    __tablename__ = 'products'

    id = db.Column(db.Integer, primary_key=True)
    sku = db.Column(db.String(30), unique=True)
    name = db.Column(db.String(255), nullable=False)

    def __unicode__(self):
        return self.name

@app.route("/cart/add/<sku>")
def add_to_cart_view(sku):
    product = Product.query.filter_by(sku=sku).first()

    if product is not None:
        session['cart'] = session.get('cart') or dict()
        item = session['cart'].get(product.sku) or dict()
        item['qty'] = item.get('qty', 0) + 1
        session['cart'][product.sku] = item
        flash(u'%s add to cart. Total: %d' % (product, item['qty']))

    return render_template('cart.html')

def init():
    """
    Initializes and populates the database
    """
    db.create_all()

    if Product.query.count() == 0:
        db.session.add_all([
            Product(sku='010', name='Boots'),
            Product(sku='020', name='Gauntlets'),
            Product(sku='030', name='Helmets'),
        ])
        db.session.commit()

if __name__ == '__main__':
    app.debug = True

    with app.test_request_context():
        init()

    app.run()
# == END
# cart.html
<html><head>
  <title>Cart</title>
</head><body>
{% with messages = get_flashed_messages() %}
  {% if messages %}
  <ul>
    {% for message in messages %}
    <li>{{ message }}</li>
    {% endfor %}
  {% endif %}
  </ul>
{% endwith %}
</body></html>

在示例中,我们定义了一个非常简单的产品模型,带有 ID、名称、sku(用于在商店中识别产品的特殊字段),以及一个视图,将请求的产品添加到用户会话中的购物车中。正如您所看到的,我们并不假设会话中有任何数据,始终保持谨慎。我们也不需要在更改后“保存”会话,因为 Flask 足够聪明,会自动注意到您的会话已经更改并保存它……实际上,这里有一个小技巧。Flask 会话只能检测到会话是否被修改,如果您修改了它的第一级值。例如:

session['cart'] = dict()  # new cart
# modified tells me if session knows it was changed
assert session.modified == True
session.modified = False  # we force it to think it was not meddled with
session['cart']['item'] = dict()
# session does not know that one of its children was modified
assert session.modified == False
# we tell it, forcing a update
session.modified =True
# session will be saved, now

现在运行您的项目,并在浏览器中打开 URL http://localhost:5000/cart/add/010。看到每次重新加载时计数器是如何增加的吗?嗯,那就是会话在工作!

练习

让我们把知识付诸实践吧?尝试制作一个商店网站应用,比如一个在线宠物商店。它应该有宠物服务,例如洗澡和兽医咨询,还有一个小商店,出售宠物配饰。这应该足够简单(很多工作!但是简单)。

总结

这是一个密集的章节。我们概述了重要的概念——如性能和可维护性、生产力和质量——快速讨论了过早优化和过度工程化,并将我们的努力集中在学习如何用 Flask 编写更好的代码上。

蓝图允许您使用 Flask 创建强大的大型项目,并通过一个完整的示例进行了讨论;我们学习了如何记录到文件和邮件以及每个的重要性,与 Flask-DebugToolbar 度过了愉快的时光(非常方便!),并且将默认的会话设置和使用牢记在心。

你现在是一个有能力的 Flask 开发者。我感到非常自豱!

就像一个人在尝试漂移之前先学会开车一样,我们将在下一章开始我们的 Flask 漂移。我们的重点将是利用 Flask 提供的广泛扩展生态系统来创建令人惊叹的项目。这将非常有趣!到时见!

第九章:扩展,我是如何爱你

我们已经在前几章中使用扩展来增强我们的示例;Flask-SQLAlchemy 用于连接到关系数据库,Flask-MongoEngine 用于连接到 MongoDB,Flask-WTF 用于创建灵活可重用的表单,等等。扩展是一种很好的方式,可以在不妨碍您的代码的情况下为项目添加功能,如果您喜欢我们迄今为止所做的工作,您会喜欢这一章,因为它专门介绍了扩展!

在本章中,我们将了解一些迄今为止忽视的非常流行的扩展。我们要开始了吗?

如何配置扩展

Flask 扩展是您导入的模块,(通常)初始化,并用于与第三方库集成。它们通常是从flask.ext.<extension_name>(这是扩展模式的一部分)导入的,并应该在 PyPi 存储库中以 BSD、MIT 或其他不太严格的许可证下可用。

扩展最好有两种状态:未初始化和已初始化。这是一个好的做法,因为在实例化扩展时,您的 Flask 应用程序可能不可用。我们在上一章的示例中只有在主模块中导入 Flask-SQLAlchemy 后才进行初始化。好的,知道了,但初始化过程为何重要呢?

嗯,正是通过初始化,扩展才能从应用程序中获取其配置。例如:

from flask import Flask
import logging

# set configuration for your Flask application or extensions
class Config(object):
    LOG_LEVEL = logging.WARNING

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

在上面的代码中,我们创建了一个配置类,并使用config.from_object加载了它。这样,LOG_LEVEL就可以在所有扩展中使用,通过对应用实例的控制。

app.config['LOG_LEVEL']

将配置加载到app.config的另一种方法是使用环境变量。这种方法在部署环境中特别有用,因为您不希望将敏感的部署配置存储在版本控制存储库中(这是不安全的!)。它的工作原理如下:

…
app.config.from_envvar('PATH_TO_CONFIGURATION')

如果PATH_TO_CONFIGURATION设置为 Python 文件路径,例如/home/youruser/someconfig.py,那么someconfig.py将加载到配置中。像这样做:

# in the console
export  PATH_TO_CONFIGURATION=/home/youruser/someconfig.py

然后创建配置:

# someconfig.py
import logging
LOG_LEVEL = logging.WARNING

早期的配置方案都有相同的结果。

提示

请注意,from_envvar将从运行项目的用户加载环境变量。如果将环境变量导出到您的用户并作为另一个用户(如 www-data)运行项目,则可能无法找到您的配置。

Flask-Principal 和 Flask-Login(又名蝙蝠侠和罗宾)

如项目页面所述(pythonhosted.org/Flask-Principal/),Flask-Principal 是一个权限扩展。它管理谁可以访问什么以及在什么程度上。通常情况下,您应该与身份验证和会话管理器一起使用它,就像 Flask-Login 的情况一样,这是我们将在本节中学习的另一个扩展。

Flask-Principal 通过四个简单的实体处理权限:IdentityIdentityContextNeedPermission

  • Identity:这意味着 Flask-Principal 识别用户的方式。

  • IdentityContext:这意味着针对权限测试的用户上下文。它用于验证用户是否有权执行某些操作。它可以用作装饰器(阻止未经授权的访问)或上下文管理器(仅执行)。

Need是您需要满足的标准(啊哈时刻!),以便做某事,比如拥有角色或权限。Principal 提供了一些预设的需求,但您也可以轻松创建自己的需求,因为 Need 只是一个命名元组,就像这样一个:

from collections import namedtuplenamedtuple('RoleNeed', ['role', 'admin'])
  • 权限:这是一组需要,应满足以允许某事。将其解释为资源的守护者。

鉴于我们已经设置好了我们的授权扩展,我们需要针对某些内容进行授权。一个常见的情况是将对管理界面的访问限制为管理员(不要说任何话)。为此,我们需要确定谁是管理员,谁不是。Flask-Login 可以通过提供用户会话管理(登录和注销)来帮助我们。让我们尝试一个例子。首先,确保安装了所需的依赖项:

pip install flask-wtf flask-login flask-principal flask-sqlalchemy

然后:

# coding:utf-8
# this example is based in the examples available in flask-login and flask-principal docs

from flask_wtf import Form

from wtforms import StringField, PasswordField, ValidationError
from wtforms import validators

from flask import Flask, flash, render_template, redirect, url_for, request, session, current_app
from flask.ext.login import UserMixin
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager, login_user, logout_user, login_required, current_user
from flask.ext.principal import Principal, Permission, Identity, AnonymousIdentity, identity_changed
from flask.ext.principal import RoleNeed, UserNeed, identity_loaded

principal = Principal()
login_manager = LoginManager()
login_manager.login_view = 'login_view'
# you may also overwrite the default flashed login message
# login_manager.login_message = 'Please log in to access this page.'
db = SQLAlchemy()

# Create a permission with a single Need
# we use it to see if an user has the correct rights to do something
admin_permission = Permission(RoleNeed('admin'))

由于我们的示例现在太大了,我们将逐步理解它。首先,我们进行必要的导入并创建我们的扩展实例。我们为login_manager设置login_view,以便它知道如果用户尝试访问需要用户身份验证的页面时应该重定向到哪里。请注意,Flask-Principal 不处理或跟踪已登录的用户。这是 Flask-Login 的魔术!

我们还创建了我们的admin_permission。我们的管理员权限只有一个需求:角色管理员。这样,我们定义了我们的权限接受用户时,这个用户需要拥有角色admin

# UserMixin implements some of the methods required by Flask-Login
class User(db.Model, UserMixin):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    active = db.Column(db.Boolean, default=False)
    username = db.Column(db.String(60), unique=True, nullable=False)
    password = db.Column(db.String(20), nullable=False)
    roles = db.relationship(
        'Role', backref='roles', lazy='dynamic')

    def __unicode__(self):
        return self.username

    # flask login expects an is_active method in your user model
    # you usually inactivate a user account if you don't want it
    # to have access to the system anymore
    def is_active(self):
        """
        Tells flask-login if the user account is active
        """
        return self.active

class Role(db.Model):
    """
    Holds our user roles
    """
    __tablename__ = 'roles'
    name = db.Column(db.String(60), primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

    def __unicode__(self):
        return self.name

我们在这里有两个模型,一个用于保存我们的用户信息,另一个用于保存我们的用户角色。角色通常用于对用户进行分类,比如admin;您的系统中可能有三个管理员,他们都将拥有管理员角色。因此,如果权限正确配置,他们都将能够执行“管理员操作”。请注意,我们为用户定义了一个is_active方法。该方法是必需的,我建议您始终覆盖它,即使UserMixin已经提供了实现。is_active用于告诉login用户是否活跃;如果不活跃,他可能无法登录。

class LoginForm(Form):
    def get_user(self):
        return User.query.filter_by(username=self.username.data).first()

    user = property(get_user)

    username = StringField(validators=[validators.InputRequired()])
    password = PasswordField(validators=[validators.InputRequired()])

    def validate_username(self, field):
        "Validates that the username belongs to an actual user"
        if self.user is None:
            # do not send a very specific error message here, otherwise you'll
            # be telling the user which users are available in your database
            raise ValidationError('Your username and password did not match')

    def validate_password(self, field):
        username = field.data
        user = User.query.get(username)

        if user is not None:
            if not user.password == field.data:
                raise ValidationError('Your username and password did not match')

在这里,我们自己编写了LoginForm。你可能会说:“为什么不使用model_form呢?”嗯,在这里使用model_form,您将不得不使用您的应用程序初始化数据库(您目前还没有)并设置上下文。太麻烦了。

我们还定义了两个自定义验证器,一个用于检查username是否有效,另一个用于检查passwordusername是否匹配。

提示

请注意,我们为这个特定表单提供了非常广泛的错误消息。我们这样做是为了避免向可能的攻击者提供太多信息。

class Config(object):
    "Base configuration class"
    DEBUG = False
    SECRET_KEY = 'secret'
    SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/ex03.db'

class Dev(Config):
    "Our dev configuration"
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dev.db'

def setup(app):
    # initializing our extensions ; )
    db.init_app(app)
    principal.init_app(app)
    login_manager.init_app(app)

    # adding views without using decorators
    app.add_url_rule('/admin/', view_func=admin_view)
    app.add_url_rule('/admin/context/', view_func=admin_only_view)
    app.add_url_rule('/login/', view_func=login_view, methods=['GET', 'POST'])
    app.add_url_rule('/logout/', view_func=logout_view)

    # connecting on_identity_loaded signal to our app
    # you may also connect using the @identity_loaded.connect_via(app) decorator
    identity_loaded.connect(on_identity_loaded, app, False)

# our application factory
def app_factory(name=__name__, config=Dev):
    app = Flask(name)
    app.config.from_object(config)
    setup(app)
    return app

在这里,我们定义了我们的配置对象,我们的app设置和应用程序工厂。我会说,设置是棘手的部分,因为它使用app方法注册视图,而不是装饰器(是的,与使用@app.route相同的结果),并且我们将我们的identity_loaded信号连接到我们的应用程序,以便用户身份在每个请求中都被加载和可用。我们也可以将其注册为装饰器,就像这样:

@identity_loaded.connect_via(app)

# we use the decorator to let the login_manager know of our load_user
# userid is the model id attribute by default
@login_manager.user_loader
def load_user(userid):
    """
    Loads an user using the user_id

    Used by flask-login to load the user with the user id stored in session
    """
    return User.query.get(userid)

def on_identity_loaded(sender, identity):
    # Set the identity user object
    identity.user = current_user

    # in case you have resources that belong to a specific user
    if hasattr(current_user, 'id'):
        identity.provides.add(UserNeed(current_user.id))

    # Assuming the User model has a list of roles, update the
    # identity with the roles that the user provides
    if hasattr(current_user, 'roles'):
        for role in current_user.roles:
            identity.provides.add(RoleNeed(role.name))

load_user 函数是 Flask-Login 要求的,用于使用会话存储中存储的userid加载用户。如果没有找到userid,它应该返回None。不要在这里抛出异常。

on_identity_loaded 被注册到 identity_loaded 信号,并用于加载存储在模型中的身份需求。这是必需的,因为 Flask-Principal 是一个通用解决方案,不知道您如何存储权限。

def login_view():
    form = LoginForm()

    if form.validate_on_submit():
        # authenticate the user...
        login_user(form.user)

        # Tell Flask-Principal the identity changed
        identity_changed.send(
            # do not use current_app directly
            current_app._get_current_object(),
            identity=Identity(form.user.id))
        flash("Logged in successfully.")
        return redirect(request.args.get("next") or url_for("admin_view"))

    return render_template("login.html", form=form)

@login_required  # you can't logout if you're not logged
def logout_view():
    # Remove the user information from the session
    # Flask-Login can handle this on its own = ]
    logout_user()

    # Remove session keys set by Flask-Principal
    for key in ('identity.name', 'identity.auth_type'):
        session.pop(key, None)

    # Tell Flask-Principal the user is anonymous
    identity_changed.send(
        current_app._get_current_object(),
        identity=AnonymousIdentity())

    # it's good practice to redirect after logout
    return redirect(request.args.get('next') or '/')

login_viewlogout_view 就像它们的名字一样:一个用于认证,另一个用于取消认证用户。在这两种情况下,您只需确保调用适当的 Flask-Login 函数(login_userlogout_user),并发送适当的 Flask-Principal 信号(并在注销时清除会话)。

# I like this approach better ...
@login_required
@admin_permission.require()
def admin_view():
    """
    Only admins can access this
    """
    return render_template('admin.html')

# Meh ...
@login_required
def admin_only_view():
    """
    Only admins can access this
    """
    with admin_permission.require():
        # using context
        return render_template('admin.html')

最后,我们有我们的实际视图:admin_viewadmin_only_view。它们两者都做同样的事情,它们检查用户是否使用 Flask-Login 登录,然后检查他们是否有足够的权限来访问视图。这里的区别是,在第一种情况下,admin_view使用权限作为装饰器来验证用户的凭据,并在第二种情况下作为上下文。

def populate():
    """
    Populates our database with a single user, for testing ; )

    Why not use fixtures? Just don't wanna ...
    """
    user = User(username='student', password='passwd', active=True)
    db.session.add(user)
    db.session.commit()
    role = Role(name='admin', user_id=user.id)
    db.session.add(role)
    db.session.commit()

if __name__ == '__main__':
    app = app_factory()

    # we need to use a context here, otherwise we'll get a runtime error
    with app.test_request_context():
        db.drop_all()
        db.create_all()
        populate()

    app.run()

populate 用于在我们的数据库中添加适当的用户和角色,以便您进行测试。

提示

关于我们之前的例子需要注意的一点:我们在用户数据库中使用了纯文本。在实际的代码中,你不想这样做,因为用户通常会在多个网站使用相同的密码。如果密码是纯文本,任何访问数据库的人都能知道它并测试它是否与敏感网站匹配。flask.pocoo.org/snippets/54/中提供的解决方案可能有助于避免这种情况。

现在这是一个你可以与前面的代码一起使用的base.html模板的示例:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{% block title %}{% endblock %}</title>

  <link rel="stylesheet" media="screen,projection"
    href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/css/materialize.min.css" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
  <style type="text/css">
    .messages{
      position: fixed;
      list-style: none;
      margin:0px;
      padding: .5rem 2rem;
      bottom: 0; left: 0;
      width:100%;
      background-color: #abc;
      text-align: center;
    }
  </style>
</head>
<body>
  {% with messages = get_flashed_messages() %}
    {% if messages %}
    <ul class='messages'>
        {% for message in messages %}
        <li>{{ message }}</li>
        {% endfor %}
    </ul>
    {% endif %}
  {% endwith %}

  <header>
     <nav>
      <div class="container nav-wrapper">
        {% if current_user.is_authenticated() %}
        <span>Welcome to the admin interface, {{ current_user.username }}</span>
        {% else %}<span>Welcome, stranger</span>{% endif %}

        <ul id="nav-mobile" class="right hide-on-med-and-down">
          {% if current_user.is_authenticated() %}
          <li><a href="{{ url_for('logout_view') }}?next=/admin/">Logout</a></li>
          {% else %}
          <li><a href="{{ url_for('login_view') }}?next=/admin/">Login</a></li>
          {% endif %}
        </ul>
      </div>
    </nav>
  </header>
  <div class="container">
    {% block content %}{% endblock %}
  </div>
  <script type="text/javascript" src="img/jquery-2.1.1.min.js"></script>
  <script src="img/materialize.min.js"></script>
</body>
</html>

请注意,我们在模板中使用current_user.is_authenticated()来检查用户是否经过身份验证,因为current_user在所有模板中都可用。现在,尝试自己编写login.htmladmin.html,并扩展base.html

管理员就像老板一样

Django 之所以如此出名的原因之一是因为它有一个漂亮而灵活的管理界面,我们也想要一个!

就像 Flask-Principal 和 Flask-Login 一样,我们将用来构建我们的管理界面的扩展 Flask-Admin 不需要特定的数据库来使用。你可以使用 MongoDB 作为关系数据库(与 SQLAlchemy 或 PeeWee 一起),或者你喜欢的其他数据库。

与 Django 相反,Django 的管理界面专注于应用程序/模型,而 Flask-Admin 专注于页面/模型。你不能(没有一些重编码)将整个蓝图(Flask 的 Django 应用程序等效)加载到管理界面中,但你可以为你的蓝图创建一个页面,并将蓝图模型注册到其中。这种方法的一个优点是你可以轻松选择所有模型将被列出的位置。

在我们之前的例子中,我们创建了两个模型来保存我们的用户和角色信息,所以,让我们为这两个模型创建一个简单的管理员界面。我们确保我们的依赖已安装:

pip install flask-admin

然后:

# coding:utf-8

from flask import Flask
from flask.ext.admin import Admin
from flask.ext.admin.contrib.sqla import ModelView
from flask.ext.login import UserMixin
from flask.ext.sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model, UserMixin):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    active = db.Column(db.Boolean, default=False)
    username = db.Column(db.String(60), unique=True, nullable=False)
    password = db.Column(db.String(20), nullable=False)
    roles = db.relationship(
        'Role', backref='roles', lazy='dynamic')

    def __unicode__(self):
        return self.username

    # flask login expects an is_active method in your user model
    # you usually inactivate a user account if you don't want it
    # to have access to the system anymore
    def is_active(self):
        """
        Tells flask-login if the user account is active
        """
        return self.active

class Role(db.Model):
    """
    Holds our user roles
    """
    __tablename__ = 'roles'
    name = db.Column(db.String(60), primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

    def __unicode__(self):
        return self.name

# Flask and Flask-SQLAlchemy initialization here
admin = Admin()
admin.add_view(ModelView(User, db.session, category='Profile'))
admin.add_view(ModelView(Role, db.session, category='Profile'))

def app_factory(name=__name__):
    app = Flask(name)
    app.debug = True
    app.config['SECRET_KEY'] = 'secret'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/ex04.db'

    db.init_app(app)
    admin.init_app(app)
    return app

if __name__ == '__main__':
    app = app_factory()

    # we need to use a context here, otherwise we'll get a runtime error
    with app.test_request_context():
        db.drop_all()
        db.create_all()

    app.run()

在这个例子中,我们创建并初始化了admin扩展,然后使用ModelView向其注册我们的模型,这是一个为我们的模型创建CRUD的特殊类。运行此代码,尝试访问http://127.0.0.1:5000/admin/;您将看到一个漂亮的管理界面,顶部有一个主页链接,下面是一个包含两个链接的个人资料下拉菜单,指向我们的模型 CRUDs 的用户角色。这只是一个非常基本的例子,不算太多,因为你不能拥有一个像那样对所有用户开放的管理界面。

我们向管理员视图添加身份验证和权限验证的一种方法是通过扩展ModelViewIndexView。我们还将使用一个称为mixin的很酷的设计模式:

# coding:utf-8
# permissions.py

from flask.ext.principal import RoleNeed, UserNeed, Permission
from flask.ext.principal import Principal

principal = Principal()

# admin permission role
admin_permission = Permission(RoleNeed('admin'))

# END of FILE

# coding:utf-8
# admin.py

from flask import g
from flask.ext.login import current_user, login_required
from flask.ext.admin import Admin, AdminIndexView, expose
from flask.ext.admin.contrib.sqla import ModelView

from permissions import *

class AuthMixinView(object):
    def is_accessible(self):
        has_auth = current_user.is_authenticated()
        has_perm = admin_permission.allows(g.identity)
        return has_auth and has_perm

class AuthModelView(AuthMixinView, ModelView):
    @expose()
    @login_required
    def index_view(self):
        return super(ModelView, self).index_view()

class AuthAdminIndexView(AuthMixinView, AdminIndexView):
    @expose()
    @login_required
    def index_view(self):
        return super(AdminIndexView, self).index_view()

admin = Admin(name='Administrative Interface', index_view=AuthAdminIndexView())

我们在这里做什么?我们重写is_accessible方法,这样没有权限的用户将收到一个禁止访问的消息,并重写AdminIndexViewModelViewindex_view,添加login_required装饰器,将未经身份验证的用户重定向到登录页面。admin_permission验证给定的身份是否具有所需的权限集——在我们的例子中是RoleNeed('admin')

提示

如果你想知道 mixin 是什么,请尝试这个链接stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful

由于我们的模型已经具有创建、读取、更新、删除CRUD)和权限控制访问,我们如何修改我们的 CRUD 以仅显示特定字段,或阻止添加其他字段?

就像 Django Admin 一样,Flask-Admin 允许你通过设置类属性来更改你的 ModelView 行为。我个人最喜欢的几个是这些:

  • can_create: 这允许用户使用 CRUD 创建模型。

  • can_edit: 这允许用户使用 CRUD 更新模型。

  • can_delete: 这允许用户使用 CRUD 删除模型。

  • list_templateedit_templatecreate_template:这些是默认的 CRUD 模板。

  • list_columns: 这意味着列在列表视图中显示。

  • column_editable_list:这表示可以在列表视图中编辑的列。

  • form:这是 CRUD 用来编辑和创建视图的表单。

  • form_args:这用于传递表单字段参数。像这样使用它:

form_args = {'form_field_name': {'parameter': 'value'}}  # parameter could be name, for example
  • form_overrides:像这样使用它来覆盖表单字段:
form_overrides = {'form_field': wtforms.SomeField}
  • form_choices:允许你为表单字段定义选择。像这样使用它:
form_choices = {'form_field': [('value store in db', 'value display in the combo box')]}

一个例子看起来像这样:

class AuthModelView(AuthMixinView, ModelView):
    can_edit= False
    form = MyAuthForm

    @expose()
    @login_required
    def index_view(self):
        return super(ModelView, self).index_view()

自定义页面

现在,如果你想要在管理界面中添加一个自定义的报告页面,你肯定不会使用模型视图来完成这个任务。对于这些情况,像这样添加一个自定义的BaseView

# coding:utf-8
from flask import Flask
from flask.ext.admin import Admin, BaseView, expose

class ReportsView(BaseView):
    @expose('/')
    def index(self):
        # make sure reports.html exists
        return self.render('reports.html')

app = Flask(__name__)
admin = Admin(app)
admin.add_view(ReportsView(name='Reports Page'))

if __name__ == '__main__':
    app.debug = True
    app.run()

现在你有了一个带有漂亮的报告页面链接的管理界面。不要忘记编写一个reports.html页面,以使前面的示例工作。

那么,如果你不希望链接显示在导航栏中,因为你已经在其他地方有了它,怎么办?覆盖BaseView.is_visible方法,因为它控制视图是否会出现在导航栏中。像这样做:

class ReportsView(BaseView):
…
  def is_visible(self):
    return False

摘要

在这一章中,我们只是学习了一些关于用户授权和认证的技巧,甚至尝试创建了一个管理界面。这是相当多的知识,将在你日常编码中帮助你很多,因为安全性(确保人们只与他们可以和应该互动的内容进行互动)是一个非常普遍的需求。

现在,我的朋友,你知道如何开发健壮的 Flask 应用程序,使用 MVC、TDD、与权限和认证控制集成的关系型和 NoSQL 数据库;表单;如何实现跨站点伪造保护;甚至如何使用开箱即用的管理工具。

我们的研究重点是了解 Flask 开发世界中所有最有用的工具(当然是我认为的),以及如何在一定程度上使用它们。由于范围限制,我们没有深入探讨任何一个,但基础知识肯定是展示过的。

现在,你可以进一步提高对每个介绍的扩展和库的理解,并寻找新的扩展。下一章也试图在这个旅程中启发你,建议阅读材料、文章和教程(等等)。

希望你到目前为止已经喜欢这本书,并且对最后的笔记感到非常愉快。

第十章:现在怎么办?

Flask 目前是最受欢迎的 Web 框架,因此为它找到在线阅读材料并不难。例如,快速在谷歌上搜索肯定会给你找到一两篇你感兴趣的好文章。尽管如此,像部署这样的主题,尽管在互联网上讨论得很多,但仍然会在我们的网页战士心中引起怀疑。因此,我们在最后一章中提供了一个很好的“像老板一样部署你的 Flask 应用程序”的逐步指南。除此之外,我们还会建议你一些非常特别的地方,那里的知识就在那里,浓厚而丰富,等着你去获取智慧。通过这一章,你将能够将你的产品从代码部署到服务器,也许,只是也许,能够得到一些应得的高分!欢迎来到这一章,在这里代码遇见服务器,你遇见世界!

你的部署比我的前任好

部署不是每个人都熟悉的术语;如果你最近还不是一个 Web 开发人员,你可能对它不太熟悉。以一种粗犷的斯巴达方式,我们可以将部署定义为准备和展示你的应用程序给世界的行为,确保所需的资源可用,并对其进行调整,因为适合开发阶段的配置与适合部署的配置是不同的。在 Web 开发的背景下,我们谈论的是一些非常具体的行动:

  • 将你的代码放在服务器上

  • 设置你的数据库

  • 设置你的 HTTP 服务器

  • 设置你可能使用的其他服务

  • 将所有内容联系在一起

将你的代码放在服务器上

首先,什么是服务器?我们所说的服务器是指具有高可靠性、可用性和可维护性(RAS)等服务器特性的计算机。这些特性使服务器中运行的应用程序获得一定程度的信任,即使在出现任何环境问题(如硬件故障)之后,服务器也会继续运行。

在现实世界中,人们有预算,一个普通的计算机(你在最近的商店买的那种)很可能是运行小型应用程序的最佳选择,因为“真正的服务器”非常昂贵。对于小项目预算(现在也包括大项目),创建了一种称为服务器虚拟化的强大解决方案,其中昂贵的高 RAS 物理服务器的资源(内存、CPU、硬盘等)被虚拟化成虚拟机(VM),它们就像真实硬件的较小(更便宜)版本一样。像 DigitalOcean(https://digitalocean.com/)、Linode(https://www.linode.com/)和 RamNode(https://www.ramnode.com/)这样的公司专注于向公众提供廉价可靠的虚拟机。

现在,鉴于我们的 Web 应用程序已经准备就绪(我的意思是,我们的最小可行产品已经准备就绪),我们必须在某个对我们的目标受众可访问的地方运行代码。这通常意味着我们需要一个 Web 服务器。从前面一段提到的公司中选择两台便宜的虚拟机,使用 Ubuntu 进行设置,然后让我们开始吧!

设置你的数据库

关于数据库,部署过程中你应该知道的最基本的事情之一是,最好的做法是让你的数据库和 Web 应用程序在不同的(虚拟)机器上运行。你不希望它们竞争相同的资源,相信我。这就是为什么我们雇了两台虚拟服务器——一台将运行我们的 HTTP 服务器,另一台将运行我们的数据库。

让我们开始设置数据库服务器;首先,我们将我们的 SSH 凭据添加到远程服务器,这样我们就可以在不需要每次输入远程服务器用户密码的情况下进行身份验证。在此之前,如果你没有它们,生成你的 SSH 密钥,就像这样:

# ref: https://help.github.com/articles/generating-ssh-keys/
# type a passphrase when asked for one
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

现在,假设您的虚拟机提供程序为您的远程机器提供了 IP 地址、root 用户和密码,我们将如下创建一个无密码的 SSH 身份验证与我们的服务器:

# type the root password when requested
ssh-copy-id root@ipaddress

现在,退出您的远程终端,尝试 SSH root@ipaddress。密码将不再被请求。

第二步!摆脱非数据库的东西,比如 Apache,并安装 Postgres(www.postgresql.org/),迄今为止最先进的开源数据库:

# as root
apt-get purge apache2-*
apt-get install postgresql
# type to check which version of postgres was installed (most likely 9.x)
psql -V

现在我们设置数据库。

将默认用户 Postgres 连接到角色postgres

sudo -u postgres psql

为我们的项目创建一个名为mydb的数据库:

CREATE DATABASE mydb;

创建一个新的用户角色来访问我们的数据库:

CREATE USER you WITH PASSWORD 'passwd'; # please, use a strong password
# We now make sure "you" can do whatever you want with mydb
# You don't want to keep this setup for long, be warned
GRANT ALL PRIVILEGES ON DATABASE mydb TO you;

到目前为止,我们已经完成了很多工作。首先,我们删除了不必要的包(只有很少);安装了我们的数据库 Postgres 的最新支持版本;创建了一个新的数据库和一个新的“用户”;并授予了我们的用户对我们的新数据库的完全权限。让我们了解每一步。

我们首先删除 Apache2 等内容,因为这是一个数据库服务器设置,所以没有必要保留 Apache2 包。根据安装的 Ubuntu 版本,您甚至需要删除其他包。这里的黄金法则是:安装的包越少,我们就要关注的包就越少。只保留最少的包。

然后我们安装 Postgres。根据您的背景,您可能会问——为什么是 Postgres 而不是 MariaDB/MySQL?嗯,嗯,亲爱的读者,Postgres 是一个完整的解决方案,支持 ACID,文档(JSONB)存储,键值存储(使用 HStore),索引,文本搜索,服务器端编程,地理定位(使用 PostGIS)等等。如果您知道如何安装和使用 Postgres,您就可以在一个单一的解决方案中访问所有这些功能。我也更喜欢它比其他开源/免费解决方案,所以我们将坚持使用它。

安装 Postgres 后,我们必须对其进行配置。与我们迄今为止使用的关系数据库解决方案 SQLite 不同,Postgres 具有基于角色的强大权限系统,控制着可以被访问或修改的资源,以及由谁访问或修改。这里的主要概念是,角色是一种非常特殊的组,它可能具有称为权限的权限,或者与之相关或包含它的其他组。例如,在psql控制台内运行的CREATE USER命令(Postgres 的交互式控制台,就像 Python 的)实际上并不是创建一个用户;实际上,它是在创建一个具有登录权限的新角色,这类似于用户概念。以下命令等同于psql内的创建用户命令:

CREATE ROLE you WITH LOGIN;

现在,朝着我们最后的目标,有GRANT命令。为了允许角色执行操作,我们授予它们权限,比如登录权限,允许我们的“用户”登录。在我们的示例中,我们授予您对数据库mydb的所有可用权限。我们这样做是为了能够创建表,修改表等等。通常您不希望您的生产 Web 应用程序数据库用户(哇!)拥有所有这些权限,因为在发生安全漏洞时,入侵者将能够对您的数据库执行任何操作。因为通常(咳咳从不!)不会在用户交互中更改数据库结构,所以在 Web 应用程序中使用一个权限较低的用户并不是问题。

提示

PgAdmin 是一个令人惊叹的、用户友好的、Postgres 管理应用程序。只需使用 SSH 隧道(www.pgadmin.org/docs/dev/connect.html),就可以快乐了!

现在测试您的数据库设置是否正常工作。从控制台连接到它:

psql -U user_you -d database_mydb -h 127.0.0.1 -W

在被要求时输入你的密码。我们之前的命令实际上是我们在使用 Postgres 时使用的一个技巧,因为我们是通过网络接口连接到数据库的。默认情况下,Postgres 假设你试图使用与你的系统用户名相同的角色和数据库进行连接。除非你像我们一样通过网络接口连接,否则你甚至不能以与你的系统用户名不同的角色名称进行连接。

设置 web 服务器

设置你的 web 服务器会更加复杂,因为它涉及修改更多的文件,并确保它们之间的配置是稳固的,但我们会做到的,你会看到的。

首先,我们要确保我们的项目代码在我们的 web 服务器上(这不是与数据库服务器相同的服务器,对吧?)。我们可以以多种方式之一来做到这一点:使用 FTP(请不要),简单的 fabric 加 rsync,版本控制,或者版本加 fabric(开心脸!)。让我们看看如何做后者。

假设你已经在你的 web 服务器虚拟机中创建了一个名为myuser的常规用户,请确保已经安装了 fabric:

sudo apt-get install python-dev
pip install fabric

还有,在你的项目根目录中创建一个名为fabfile.py的文件:

# coding:utf-8

from fabric.api import *
from fabric.contrib.files import exists

env.linewise = True
# forward_agent allows you to git pull from your repository
# if you have your ssh key setup
env.forward_agent = True
env.hosts = ['your.host.ip.address']

def create_project():
    if not exists('~/project'):
        run('git clone git://path/to/repo.git')

def update_code():
    with cd('~/project'):
        run('git pull')
def reload():
    "Reloads project instance"
    run('touch --no-dereference /tmp/reload')

有了上述代码和安装了 fabric,假设你已经将你的 SSH 密钥复制到了远程服务器,并已经与你的版本控制提供商(例如githubbitbucket)进行了设置,create_projectupdate_code就可以使用了。你可以像这样使用它们:

fab create_project  # creates our project in the home folder of our remote web server
fab update_code  # updates our project code from the version control repository

这非常容易。第一条命令将你的代码放入存储库,而第二条命令将其更新到你的最后一次提交。

我们的 web 服务器设置将使用一些非常流行的工具:

  • uWSGI:这用于应用服务器和进程管理

  • Nginx:这用作我们的 HTTP 服务器

  • UpStart:这用于管理我们的 uWSGI 生命周期

UpStart 已经随 Ubuntu 一起提供,所以我们以后会记住它。对于 uWSGI,我们需要像这样安装它:

pip install uwsgi

现在,在你的虚拟环境bin文件夹中,会有一个 uWSGI 命令。记住它的位置,因为我们很快就会需要它。

在你的项目文件夹中创建一个wsgi.py文件,内容如下:

# coding:utf-8
from main import app_factory

app = app_factory(name="myproject")

uWSGI 使用上面的文件中的应用实例来连接到我们的应用程序。app_factory是一个创建应用程序的工厂函数。到目前为止,我们已经看到了一些。只需确保它返回的应用程序实例已经正确配置。就应用程序而言,这就是我们需要做的。接下来,我们将继续将 uWSGI 连接到我们的应用程序。

我们可以在命令行直接调用我们的 uWSGI 二进制文件,并提供加载 wsgi.py 文件所需的所有参数,或者我们可以创建一个ini文件,其中包含所有必要的配置,并将其提供给二进制文件。正如你可能猜到的那样,第二种方法通常更好,因此创建一个看起来像这样的 ini 文件:

[uwsgi]
user-home = /home/your-system-username
project-name = myproject
project-path = %(user-home)/%(myproject)

# make sure paths exist
socket = %(user-home)/%(project-name).sock
pidfile = %(user-home)/%(project-name).pid
logto = /var/tmp/uwsgi.%(prj).log
touch-reload = /tmp/reload
chdir = %(project-path)
wsgi-file = %(project-path)/wsgi.py
callable = app
chmod-socket = 664

master = true
processes = 5
vacuum = true
die-on-term = true
optimize = 2

user-homeproject-nameproject-path是我们用来简化我们工作的别名。socket选项指向我们的 HTTP 服务器将用于与我们的应用程序通信的套接字文件。我们不会讨论所有给定的选项,因为这不是 uWSGI 概述,但一些更重要的选项,如touch-reloadwsgi-filecallablechmod-socket,将得到详细的解释。Touch-reload 特别有用;你指定为它的参数的文件将被 uWSGI 监视,每当它被更新/触摸时,你的应用程序将被重新加载。在一些代码更新之后,你肯定想重新加载你的应用程序。Wsgi-file 指定了哪个文件有我们的 WSGI 兼容应用程序,而callable告诉 uWSGI wsgi 文件中实例的名称(通常是 app)。最后,我们有 chmod-socket,它将我们的套接字权限更改为-rw-rw-r--,即对所有者和组的读/写权限;其他人可能只读取这个。我们需要这样做,因为我们希望我们的应用程序在用户范围内,并且我们的套接字可以从www-data用户读取,这是服务器用户。这个设置非常安全,因为应用程序不能干扰系统用户资源之外的任何东西。

我们现在可以设置我们的 HTTP 服务器,这是一个非常简单的步骤。只需按照以下方式安装 Nginx:

sudo apt-get install nginx-full

现在,您的 http 服务器在端口 80 上已经运行起来了。让我们确保 Nginx 知道我们的应用程序。将以下代码写入/etc/nginx/sites-available中的名为project的文件:

server {
    listen 80;
    server_name PROJECT_DOMAIN;

    location /media {
        alias /path/to/media;
    }
    location /static {
        alias /path/to/static;
    }

    location / {
        include         /etc/nginx/uwsgi_params;
        uwsgi_pass      unix:/path/to/socket/file.sock;
    }
}

前面的配置文件创建了一个虚拟服务器,运行在端口 80 上,监听域server_name,通过/static/media提供静态和媒体文件,并监听将所有访问指向/的路径,使用我们的套接字处理。我们现在打开我们的配置并关闭 nginx 的默认配置:

sudo rm /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/project /etc/nginx/sites-enabled/project

我们刚刚做了什么?虚拟服务器的配置文件位于/etc/nginx/sites-available/中,每当我们希望 nginx 看到一个配置时,我们将其链接到已启用的站点。在前面的配置中,我们刚刚禁用了default并通过符号链接启用了project。Nginx 不会自行注意到并加载我们刚刚做的事情;我们需要告诉它重新加载其配置。让我们把这一步留到以后。

我们需要在/etc/init中创建一个最后的文件,它将使用 upstart 将我们的 uWSGI 进程注册为服务。这部分非常简单;只需创建一个名为project.conf(或任何其他有意义的名称)的文件,内容如下:

description "uWSGI application my project"

start on runlevel [2345]
stop on runlevel [!2345]

setuid your-user
setgid www-data

exec /path/to/uwsgi --ini /path/to/ini/file.ini

前面的脚本使用我们的项目ini文件(我们之前创建的)作为参数运行 uWSGI,用户为"your-user",组为 www-data。用您的用户替换your-user(…),但不要替换www-data组,因为这是必需的配置。前面的运行级别配置只是告诉 upstart 何时启动和停止此服务。您不必进行干预。

运行以下命令行来启动您的服务:

start project

接下来重新加载 Nginx 配置,就像这样:

sudo /etc/init.d/nginx reload

如果一切顺利,媒体路径和静态路径存在,项目数据库设置指向私有网络内的远程服务器,并且上帝对您微笑,您的项目应该可以从您注册的域名访问。击掌!

StackOverflow

StackOverflow 是新的谷歌术语,用于黑客和软件开发。很多人使用它,所以有很多常见问题和很好的答案供您使用。只需花几个小时阅读关于stackoverflow.com/search?q=flask的最新趋势,您肯定会学到很多!

结构化您的项目

由于 Flask 不强制执行项目结构,您有很大的自由度来尝试最适合您的方式。大型单文件项目可行,类似 Django 的结构化项目可行,平面架构也可行;可能性很多!因此,许多项目都提出了自己建议的架构;这些项目被称为样板或骨架。它们专注于为您提供一个快速启动新 Flask 项目的方法,利用他们建议的代码组织方式。

如果您计划使用 Flask 创建一个大型 Web 应用程序,强烈建议您至少查看其中一个这些项目,因为它们可能已经面临了一些您可能会遇到的问题,并提出了解决方案:

总结

我必须承认,我写这本书是为了自己。在一个地方找到构建 Web 应用程序所需的所有知识是如此困难,以至于我不得不把我的笔记放在某个地方,浓缩起来。我希望,如果您读到这一段,您也和我一样觉得,这本书是为您写的。这是一次愉快的挑战之旅。

你现在能够构建功能齐全的 Flask 应用程序,包括安全表单、数据库集成、测试,并利用扩展功能,让你能够在短时间内创建强大的软件。我感到非常自豪!现在,去告诉你的朋友你有多棒。再见!

附言

作为一个个人挑战,拿出你一直梦想编码的项目,但从未有勇气去做的,然后制作一个 MVP(最小可行产品)。创建你想法的一个非常简单的实现,并将其发布到世界上看看;然后,给我留言。我很乐意看看你的作品!

posted @ 2024-05-20 16:50  绝不原创的飞龙  阅读(14)  评论(0编辑  收藏  举报